create-sdd-project 0.18.3 → 0.19.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 +1 -1
- package/lib/meta.js +21 -7
- package/lib/upgrade-generator.js +22 -1
- package/package.json +1 -1
- package/template/.claude/agents/code-review-specialist.md +1 -0
- package/template/.claude/commands/audit-merge.md +141 -54
- package/template/.claude/skills/development-workflow/references/merge-checklist.md +8 -0
- package/template/.gemini/agents/code-review-specialist.md +1 -0
- package/template/.gemini/commands/audit-merge-instructions.md +141 -54
- package/template/.gemini/skills/development-workflow/references/merge-checklist.md +8 -0
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 —
|
|
315
|
+
| `/audit-merge` | Automated compliance audit — 12 structural pre-merge checks (ticket, tracker, evidence, merge base, working tree, data files, CI state via `gh pr checks`) + 16 advisory drift patterns. Auto-fixes issues. |
|
|
316
316
|
|
|
317
317
|
### Spec & Plan Quality
|
|
318
318
|
|
package/lib/meta.js
CHANGED
|
@@ -12,13 +12,27 @@
|
|
|
12
12
|
* warnings on cross-version upgrades — the Codex P1 finding from the
|
|
13
13
|
* v0.16.10 cross-model review).
|
|
14
14
|
*
|
|
15
|
-
* Core invariant (Codex M1 from plan v1.0 review
|
|
16
|
-
*
|
|
17
|
-
* to
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* Core invariant (Codex M1 from plan v1.0 review, refined v0.18.4 G1):
|
|
16
|
+
* a hash in this file represents the canonical TEMPLATE output the tool
|
|
17
|
+
* last attempted to install for this path. Hashes are written/updated in
|
|
18
|
+
* three situations:
|
|
19
|
+
*
|
|
20
|
+
* 1. The tool wrote canonical output (replaced, new file, or
|
|
21
|
+
* --force-template paths) — record the new template hash.
|
|
22
|
+
* 2. v0.18.4 G1 — Case 3c fallback preserve writes the template hash to
|
|
23
|
+
* bootstrap a previously-untracked path into the hash-based decision
|
|
24
|
+
* tree for future upgrades. The file on disk is NOT modified (user's
|
|
25
|
+
* customization is preserved + .new is written); only the meta entry
|
|
26
|
+
* is recorded so the next upgrade can enter Case 2 instead of falling
|
|
27
|
+
* through Case 3 again. Records what we "attempted to install", which
|
|
28
|
+
* is the correct anchor for both: (a) the user later accepting .new
|
|
29
|
+
* → user_current == stored → Case 2a clean replace, and (b) the user
|
|
30
|
+
* keeping their customization → user_current != stored → Case 2b
|
|
31
|
+
* preserve (situation (3) below then applies).
|
|
32
|
+
* 3. NEVER on Case 2b preserve — when a stored hash already exists and
|
|
33
|
+
* the current file diverges from it, the prior baseline is kept
|
|
34
|
+
* untouched. Otherwise the user's customized content would be hashed
|
|
35
|
+
* and silently overwritten on the next upgrade.
|
|
22
36
|
*
|
|
23
37
|
* File format (schemaVersion: 1):
|
|
24
38
|
* {
|
package/lib/upgrade-generator.js
CHANGED
|
@@ -540,8 +540,21 @@ function generateUpgrade(config) {
|
|
|
540
540
|
continue;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
// Content mismatch → preserve.
|
|
543
|
+
// Content mismatch → preserve.
|
|
544
|
+
//
|
|
545
|
+
// v0.18.4 G1: bootstrap hash AFTER preserve so next upgrade enters
|
|
546
|
+
// the hash-based path (Case 2) instead of falling through Case 3c
|
|
547
|
+
// again. Recording the TEMPLATE hash (not the user's hash) is the
|
|
548
|
+
// correct semantic anchor:
|
|
549
|
+
// - If user later accepts .new → user_current == stored → Case 2a
|
|
550
|
+
// clean replace.
|
|
551
|
+
// - If user keeps customization → user_current != stored → Case 2b
|
|
552
|
+
// preserve (Codex M1 invariant then applies — no further hash
|
|
553
|
+
// updates).
|
|
554
|
+
// This is opt-in at the CALL SITE, not inside preserveFile, so
|
|
555
|
+
// Case 2b callers above remain governed by the M1 invariant.
|
|
544
556
|
preserveFile(adaptedFullTargetFallback);
|
|
557
|
+
newHashes[posixPath] = computeHash(adaptedFullTargetFallback);
|
|
545
558
|
}
|
|
546
559
|
continue;
|
|
547
560
|
}
|
|
@@ -651,7 +664,9 @@ function generateUpgrade(config) {
|
|
|
651
664
|
replaced++;
|
|
652
665
|
continue;
|
|
653
666
|
}
|
|
667
|
+
// v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
|
|
654
668
|
preserveCmd();
|
|
669
|
+
newHashes[posixPath] = computeHash(rawTemplate);
|
|
655
670
|
}
|
|
656
671
|
continue;
|
|
657
672
|
}
|
|
@@ -836,6 +851,8 @@ function generateUpgrade(config) {
|
|
|
836
851
|
}
|
|
837
852
|
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
838
853
|
preserved++;
|
|
854
|
+
// v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
|
|
855
|
+
newHashes[posix] = computeHash(adaptedTarget);
|
|
839
856
|
}
|
|
840
857
|
|
|
841
858
|
// --- d) Handle standards (smart diff) ---
|
|
@@ -979,7 +996,9 @@ function generateUpgrade(config) {
|
|
|
979
996
|
replaced++;
|
|
980
997
|
continue;
|
|
981
998
|
}
|
|
999
|
+
// v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
|
|
982
1000
|
preserveStandard();
|
|
1001
|
+
newHashes[spec.posix] = computeHash(freshAdapted);
|
|
983
1002
|
}
|
|
984
1003
|
|
|
985
1004
|
step('Updated standards files');
|
|
@@ -1055,7 +1074,9 @@ function generateUpgrade(config) {
|
|
|
1055
1074
|
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
1056
1075
|
replaced++;
|
|
1057
1076
|
} else {
|
|
1077
|
+
// v0.18.4 G1: Case 3c bootstrap (see agents site for full rationale).
|
|
1058
1078
|
preserveAgentsMd();
|
|
1079
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
1059
1080
|
}
|
|
1060
1081
|
}
|
|
1061
1082
|
|
package/package.json
CHANGED
|
@@ -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,51 @@ 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
|
+
|
|
51
91
|
### Drift Checks (added v0.18.0) — ADVISORY, not blocking
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
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
94
|
|
|
55
|
-
**
|
|
95
|
+
**13. 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
96
|
```bash
|
|
57
97
|
TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
|
|
58
98
|
PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
|
|
@@ -69,14 +109,14 @@ else
|
|
|
69
109
|
fi
|
|
70
110
|
```
|
|
71
111
|
|
|
72
|
-
**
|
|
112
|
+
**14. 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
113
|
```bash
|
|
74
114
|
awk '/^## Merge Checklist Evidence/{flag=1; next} /^## /{flag=0} flag' "$TICKET" \
|
|
75
115
|
| grep -E '^\|.*\[x\].*(to be |will |pending|TBD|Will be |to be created|next commit|aspirational)' \
|
|
76
116
|
&& flag "P2 drift: aspirational row(s) found"
|
|
77
117
|
```
|
|
78
118
|
|
|
79
|
-
**
|
|
119
|
+
**15. 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
120
|
```bash
|
|
81
121
|
# Strip checkbox prefix before comparison; use grep -Fq fixed-string match.
|
|
82
122
|
grep -E "^- \[ \].*(post-merge|operator|prod rollout|pending verification)" "$TICKET" \
|
|
@@ -89,14 +129,14 @@ while IFS= read -r item; do
|
|
|
89
129
|
done < /tmp/pm_items.txt
|
|
90
130
|
```
|
|
91
131
|
|
|
92
|
-
**
|
|
132
|
+
**16. P4 — Remote branch orphan after "deleted".** Workflow Step 6 claims `[x] branch deleted` but origin still has the branch.
|
|
93
133
|
```bash
|
|
94
134
|
BRANCH=$(grep -oE '\*\*[Bb]ranch:\*\*[[:space:]]*[^[:space:]|()]+' "$TICKET" | head -1 | sed -E 's/^\*\*[Bb]ranch:\*\*[[:space:]]*//')
|
|
95
135
|
git fetch origin --prune --quiet
|
|
96
136
|
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
137
|
```
|
|
98
138
|
|
|
99
|
-
**
|
|
139
|
+
**17. 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
140
|
```bash
|
|
101
141
|
FROZEN_COUNT=0
|
|
102
142
|
for t in docs/tickets/*.md; do
|
|
@@ -114,30 +154,49 @@ done
|
|
|
114
154
|
[ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
|
|
115
155
|
```
|
|
116
156
|
|
|
117
|
-
**
|
|
157
|
+
**18. 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
158
|
```bash
|
|
119
159
|
AC_BLOCK=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
HEADER_ACS=$(echo "$AC_BLOCK" | grep -cE '^### AC[0-9]+')
|
|
161
|
+
|
|
162
|
+
if [ "$HEADER_ACS" -gt 0 ]; then
|
|
163
|
+
ACTUAL_TOTAL=$HEADER_ACS
|
|
164
|
+
# Per-AC pass: emit one line per AC ("marked" or "deferred"). Then count.
|
|
165
|
+
PER_AC=$(echo "$AC_BLOCK" | awk '
|
|
166
|
+
function flush() { if (have) { print (deferred ? "deferred" : "marked"); have=0; deferred=0 } }
|
|
167
|
+
/^### AC[0-9]+/ { flush(); have=1; next }
|
|
168
|
+
/^## / { flush(); next }
|
|
169
|
+
have && /^\*\*Status\*\*:[[:space:]]*(Deferred|Pending|Skipped|Blocked)/ { deferred=1 }
|
|
170
|
+
END { flush() }
|
|
171
|
+
')
|
|
172
|
+
ACTUAL_MARKED=$(printf '%s\n' "$PER_AC" | grep -c '^marked$' || true)
|
|
173
|
+
else
|
|
174
|
+
ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
|
|
175
|
+
ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
|
|
176
|
+
fi
|
|
177
|
+
|
|
122
178
|
CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
|
|
123
179
|
if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
|
|
124
180
|
CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
|
|
125
181
|
CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
|
|
126
182
|
[ -n "$CLAIMED_TOTAL" ] && [ "$CLAIMED_TOTAL" != "$ACTUAL_TOTAL" ] \
|
|
127
|
-
&&
|
|
128
|
-
&& flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL"
|
|
183
|
+
&& flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
129
184
|
[ -n "$CLAIMED_MARKED" ] && [ "$CLAIMED_MARKED" != "$ACTUAL_MARKED" ] \
|
|
130
|
-
&&
|
|
131
|
-
&& flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED"
|
|
185
|
+
&& flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
132
186
|
elif [ -n "$CLAIM_LINE" ]; then
|
|
133
187
|
CLAIMED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
|
|
134
|
-
[ -n "$CLAIMED" ]
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
if [ -n "$CLAIMED" ]; then
|
|
189
|
+
[ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
|
|
190
|
+
&& flag "P6 drift: 'all $CLAIMED marked' vs actual AC total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
191
|
+
# `all N marked` IMPLIES all ACs are marked; flag when actual marked < N
|
|
192
|
+
# (e.g. some ACs deferred via `**Status**: Deferred` or unchecked `[ ]`).
|
|
193
|
+
[ "$CLAIMED" != "$ACTUAL_MARKED" ] \
|
|
194
|
+
&& flag "P6 drift: 'all $CLAIMED marked' but only $ACTUAL_MARKED actually marked (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
195
|
+
fi
|
|
137
196
|
fi
|
|
138
197
|
```
|
|
139
198
|
|
|
140
|
-
**
|
|
199
|
+
**19. 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
200
|
```bash
|
|
142
201
|
TERMINAL=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET" | grep -iE "(test|pass|green)" | grep -oE "[0-9]+/[0-9]+" | tail -1)
|
|
143
202
|
AC=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
|
|
@@ -148,7 +207,7 @@ for n in $FINAL_NUMS; do
|
|
|
148
207
|
done
|
|
149
208
|
```
|
|
150
209
|
|
|
151
|
-
**
|
|
210
|
+
**20. 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
211
|
```bash
|
|
153
212
|
WORKFLOW=$(awk '/^## Workflow Checklist/,/^## Completion Log/' "$TICKET")
|
|
154
213
|
COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
@@ -159,7 +218,7 @@ while read -r step_num; do
|
|
|
159
218
|
done <<< "$CHECKED_STEPS"
|
|
160
219
|
```
|
|
161
220
|
|
|
162
|
-
**
|
|
221
|
+
**21. 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
222
|
```bash
|
|
164
223
|
TRACKER=docs/project_notes/product-tracker.md
|
|
165
224
|
HEADER_STEP=$(grep '^\*\*Last Updated:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
@@ -168,7 +227,7 @@ DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE '(Step
|
|
|
168
227
|
&& flag "P9 drift: tracker header says $HEADER_STEP, Active Feature says $DETAIL_STEP"
|
|
169
228
|
```
|
|
170
229
|
|
|
171
|
-
**
|
|
230
|
+
**22. P10 — Duplicate Completion Log rows.** Hash `date | action | first-80-of-notes`. Duplicates suggest copy-paste error during editing.
|
|
172
231
|
```bash
|
|
173
232
|
awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
174
233
|
key = $2 "|" $3 "|" substr($4, 1, 80)
|
|
@@ -178,7 +237,7 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
|
178
237
|
| while read -r dup; do flag "P10 drift: duplicate Completion Log row: $dup"; done
|
|
179
238
|
```
|
|
180
239
|
|
|
181
|
-
**
|
|
240
|
+
**23. 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
241
|
```bash
|
|
183
242
|
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
184
243
|
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
@@ -186,18 +245,36 @@ TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
|
186
245
|
| sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
|
|
187
246
|
| sed -E 's/\*\*[[:space:]]*$//' \
|
|
188
247
|
| sed -E 's/[[:space:]]+$//')
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
TICKET_BASENAME=$(basename "$TICKET" .md)
|
|
249
|
+
# v0.18.4 P11-B: sub-scope tickets (-lite / -FU / -FU[0-9]*) close a partial
|
|
250
|
+
# scope of a parent feature; the parent tracker row stays at its parent status
|
|
251
|
+
# (typically `pending` or `in-progress`) while the sub-scope ticket reaches
|
|
252
|
+
# `Done`. P11 must NOT enforce status mapping across this boundary. Pattern
|
|
253
|
+
# scope: empirically derived from fx convention (uppercase -FU + -lite). If
|
|
254
|
+
# future projects introduce -spike / -mini / -aux variants, expand here.
|
|
255
|
+
case "$TICKET_BASENAME" in
|
|
256
|
+
*-lite|*-lite-*|*-FU|*-FU-*|*-FU[0-9]*)
|
|
257
|
+
echo "P11 N/A: $TICKET_BASENAME is a sub-scope ticket — parent tracker row status independent" >&2
|
|
258
|
+
;;
|
|
259
|
+
*)
|
|
260
|
+
FEATURE_ID=$(echo "$TICKET_BASENAME" | sed -E 's/-[a-z].*//')
|
|
261
|
+
# v0.18.4 P11-B drive-by hardening: anchor tracker lookup to pipe-table row
|
|
262
|
+
# (FEATURE_ID as first cell). Mirrors v0.18.3 P16 idiom — prevents narrative
|
|
263
|
+
# mentions earlier in the tracker from silencing or false-firing the check.
|
|
264
|
+
TRACKER_STATUS=$(grep -E "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" docs/project_notes/product-tracker.md \
|
|
265
|
+
| grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
|
|
266
|
+
case "$TICKET_STATUS" in
|
|
267
|
+
"Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
|
|
268
|
+
"Done") EXPECTED="done" ;;
|
|
269
|
+
*) EXPECTED="" ;;
|
|
270
|
+
esac
|
|
271
|
+
[ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
|
|
272
|
+
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
273
|
+
;;
|
|
195
274
|
esac
|
|
196
|
-
[ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
|
|
197
|
-
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
198
275
|
```
|
|
199
276
|
|
|
200
|
-
**
|
|
277
|
+
**24. 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.
|
|
201
278
|
```bash
|
|
202
279
|
TRACKER=docs/project_notes/product-tracker.md
|
|
203
280
|
if [ -f "$TRACKER" ]; then
|
|
@@ -217,7 +294,7 @@ if [ -f "$TRACKER" ]; then
|
|
|
217
294
|
fi
|
|
218
295
|
```
|
|
219
296
|
|
|
220
|
-
**
|
|
297
|
+
**25. 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.
|
|
221
298
|
```bash
|
|
222
299
|
KEY_FACTS=docs/project_notes/key_facts.md
|
|
223
300
|
if [ -f "$KEY_FACTS" ]; then
|
|
@@ -233,7 +310,7 @@ if [ -f "$KEY_FACTS" ]; then
|
|
|
233
310
|
fi
|
|
234
311
|
```
|
|
235
312
|
|
|
236
|
-
**
|
|
313
|
+
**26. 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.
|
|
237
314
|
```bash
|
|
238
315
|
if [ "$TICKET_STATUS" = "Done" ]; then
|
|
239
316
|
MCE_BLOCK=$(awk '
|
|
@@ -247,7 +324,7 @@ if [ "$TICKET_STATUS" = "Done" ]; then
|
|
|
247
324
|
fi
|
|
248
325
|
```
|
|
249
326
|
|
|
250
|
-
**
|
|
327
|
+
**27. 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`.
|
|
251
328
|
```bash
|
|
252
329
|
COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
253
330
|
AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
|
|
@@ -263,7 +340,7 @@ while IFS= read -r line; do
|
|
|
263
340
|
done <<< "$AC_LINES"
|
|
264
341
|
```
|
|
265
342
|
|
|
266
|
-
**
|
|
343
|
+
**28. 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.
|
|
267
344
|
```bash
|
|
268
345
|
TRACKER=docs/project_notes/product-tracker.md
|
|
269
346
|
case "$TICKET_STATUS" in
|
|
@@ -298,7 +375,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
298
375
|
```
|
|
299
376
|
## Merge Compliance Audit — [FEATURE-ID]
|
|
300
377
|
|
|
301
|
-
### Structural (1-
|
|
378
|
+
### Structural (1-12) — blocking merge gate
|
|
302
379
|
|
|
303
380
|
| # | Check | Status | Detail |
|
|
304
381
|
|---|-------|:------:|--------|
|
|
@@ -313,39 +390,48 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
313
390
|
| 9 | Merge Base | PASS | Up to date with develop |
|
|
314
391
|
| 10 | Working Tree | PASS | Clean |
|
|
315
392
|
| 11 | Data Files | PASS | N/A — no JSON seed files |
|
|
393
|
+
| 12 | CI State | PASS | PR #123 — all checks pass or pending |
|
|
316
394
|
|
|
317
395
|
**STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
|
|
318
396
|
|
|
319
|
-
### Drift (
|
|
397
|
+
### Drift (13-28) — advisory, refresh before user authorization
|
|
320
398
|
|
|
321
399
|
| # | Pattern | Status | Detail |
|
|
322
400
|
|---|---------|:------:|--------|
|
|
323
|
-
|
|
|
324
|
-
|
|
|
325
|
-
|
|
|
326
|
-
|
|
|
327
|
-
|
|
|
328
|
-
|
|
|
329
|
-
|
|
|
330
|
-
|
|
|
331
|
-
|
|
|
332
|
-
|
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
401
|
+
| 13 | P1 PR body test count stale | PASS | matches ticket terminal |
|
|
402
|
+
| 14 | P2 Aspirational Evidence rows | PASS | all rows past-tense |
|
|
403
|
+
| 15 | P3 Post-merge actions logged | PASS | N/A pre-merge |
|
|
404
|
+
| 16 | P4 Remote branch orphan | PASS | not checked pre-merge |
|
|
405
|
+
| 17 | P5 Frozen ticket Status | PASS | 0 frozen |
|
|
406
|
+
| 18 | P6 AC count off-by-N | PASS | claim matches actual (form: header) |
|
|
407
|
+
| 19 | P7 Intra-ticket test drift | PASS | final sections = terminal |
|
|
408
|
+
| 20 | P8 Completion Log gap | PASS | every [x] step has narrative |
|
|
409
|
+
| 21 | P9 Tracker header stale | PASS | header = detail |
|
|
410
|
+
| 22 | P10 Duplicate log rows | PASS | no duplicates |
|
|
411
|
+
| 23 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
|
|
412
|
+
| 24 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
|
|
413
|
+
| 25 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
|
|
414
|
+
| 26 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
|
|
415
|
+
| 27 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
|
|
416
|
+
| 28 | P16 Feature missing from tracker | PASS | feature in Features table |
|
|
339
417
|
|
|
340
418
|
**DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
|
|
341
419
|
|
|
342
420
|
### Combined verdict
|
|
343
421
|
|
|
344
|
-
- Both PASS → **READY FOR MERGE** (compliance
|
|
422
|
+
- Both PASS → **READY FOR MERGE** (compliance 12/12, drift clean)
|
|
345
423
|
- Structural fail → **NEEDS FIX — N structural blockers** (any drift noted separately)
|
|
346
424
|
- Structural pass + drift advisories → **READY FOR MERGE PENDING DRIFT CLEANUP — N advisories**
|
|
347
425
|
```
|
|
348
426
|
|
|
427
|
+
**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:
|
|
428
|
+
- Numeric ratios (`N/M`): preserve as emitted. `18/19` MUST NOT become `18/18` even when N < M.
|
|
429
|
+
- 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.
|
|
430
|
+
- "Form" annotations (e.g. P6 emits `(form: header)` or `(form: checkbox)`): preserve them so the reader can tell which AC form the ticket used.
|
|
431
|
+
- `N/A` reasons: quote the literal reason emitted by the recipe (`no PR open`, `gh CLI unavailable`, `jq unavailable`, `not checked pre-merge`).
|
|
432
|
+
|
|
433
|
+
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.
|
|
434
|
+
|
|
349
435
|
### If issues are found
|
|
350
436
|
|
|
351
437
|
Fix them directly:
|
|
@@ -354,8 +440,9 @@ Fix them directly:
|
|
|
354
440
|
- Tracker stale → update Active Session and Features table
|
|
355
441
|
- Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
|
|
356
442
|
- Data file issues → fix the data
|
|
443
|
+
- CI failures (C3) → re-run failed jobs after addressing root cause; do NOT merge while `ci-success` is BLOCKED
|
|
357
444
|
|
|
358
|
-
**Drift advisories (
|
|
445
|
+
**Drift advisories (13-28) fixes:**
|
|
359
446
|
- **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
|
|
360
447
|
- **P2 (Aspirational Evidence)** → rewrite `[x]` rows with past-tense text + commit SHA + concrete numbers
|
|
361
448
|
- **P3 (Post-merge action unlogged)** → add a Completion Log row documenting the post-merge execution with date + action + empirical result
|
|
@@ -74,6 +74,14 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
|
|
|
74
74
|
|
|
75
75
|
**Canonical form for the AC count claim:** write `AC: <marked>/<total>` — `marked` is the count of `[x]` Acceptance Criteria, `total` is the count of all AC items including any intentionally deferred `[ ]`. When all are checked use the matching form `AC: N/N` (or the shorthand `all N marked`). The `/audit-merge` P6 drift check parses both forms.
|
|
76
76
|
|
|
77
|
+
**Sub-scope ticket naming convention (recognized by `/audit-merge` P11 since v0.18.4):** when a feature is too large to close in one ticket, split it into sub-scope tickets using one of these suffixes on the basename:
|
|
78
|
+
|
|
79
|
+
- `<FEATURE_ID>-lite-<descriptor>.md` — minimal viable closure of a partial scope (e.g. `F116-lite-ci-hardening.md`)
|
|
80
|
+
- `<FEATURE_ID>-FU.md` — single follow-up closing a deferred piece
|
|
81
|
+
- `<FEATURE_ID>-FU<N>.md` — numbered follow-ups (e.g. `F-H7-FU1.md`, `F-H10-FU2.md`)
|
|
82
|
+
|
|
83
|
+
Sub-scope tickets reach `Status: Done` independently while the parent feature's tracker row stays at its parent status (typically `pending` or `in-progress`) until ALL sub-scopes close. `/audit-merge` P11 detects the suffix and emits `P11 N/A` instead of flagging the parent/sub-scope status divergence as drift.
|
|
84
|
+
|
|
77
85
|
## Action 9: Run compliance audit
|
|
78
86
|
|
|
79
87
|
Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.
|
|
@@ -19,6 +19,7 @@ Then go beyond checklist review — actively try to break the implementation:
|
|
|
19
19
|
- What happens under concurrent requests? Race conditions?
|
|
20
20
|
- What data could a malicious user inject?
|
|
21
21
|
- What if a transaction fails midway or cache is stale?
|
|
22
|
+
- **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.
|
|
22
23
|
|
|
23
24
|
## Output Format
|
|
24
25
|
|
|
@@ -48,11 +48,51 @@ 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
|
+
|
|
51
91
|
### Drift Checks (added v0.18.0) — ADVISORY, not blocking
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
Sixteen empirically-validated drift patterns. Failures are NOT blockers for the compliance verdict, but MUST be refreshed before requesting user authorization. Use BSD-grep-compatible regex (no `\K`).
|
|
54
94
|
|
|
55
|
-
**
|
|
95
|
+
**13. P1 — PR body test count stale (v0.18.3 multi-workspace extension — C1).** All PR-body ratios must appear in ticket evidence. Subset direction: PR ⊆ ticket. Three fallback cases: (a) ratios on both sides → walk each PR ratio; (b)/(c) missing on either side → emit `P1 N/A`.
|
|
56
96
|
```bash
|
|
57
97
|
TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
|
|
58
98
|
PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
|
|
@@ -69,14 +109,14 @@ else
|
|
|
69
109
|
fi
|
|
70
110
|
```
|
|
71
111
|
|
|
72
|
-
**
|
|
112
|
+
**14. P2 — Merge Checklist Evidence aspirational.** `[x]` rows with future-tense text.
|
|
73
113
|
```bash
|
|
74
114
|
awk '/^## Merge Checklist Evidence/{flag=1; next} /^## /{flag=0} flag' "$TICKET" \
|
|
75
115
|
| grep -E '^\|.*\[x\].*(to be |will |pending|TBD|Will be |to be created|next commit|aspirational)' \
|
|
76
116
|
&& flag "P2 drift: aspirational row(s) found"
|
|
77
117
|
```
|
|
78
118
|
|
|
79
|
-
**
|
|
119
|
+
**15. P3 — Post-merge actions not logged** (post-merge only).
|
|
80
120
|
```bash
|
|
81
121
|
# Strip checkbox prefix before comparison; use grep -Fq fixed-string match.
|
|
82
122
|
grep -E "^- \[ \].*(post-merge|operator|prod rollout|pending verification)" "$TICKET" \
|
|
@@ -89,14 +129,14 @@ while IFS= read -r item; do
|
|
|
89
129
|
done < /tmp/pm_items.txt
|
|
90
130
|
```
|
|
91
131
|
|
|
92
|
-
**
|
|
132
|
+
**16. P4 — Remote branch orphan.**
|
|
93
133
|
```bash
|
|
94
134
|
BRANCH=$(grep -oE '\*\*[Bb]ranch:\*\*[[:space:]]*[^[:space:]|()]+' "$TICKET" | head -1 | sed -E 's/^\*\*[Bb]ranch:\*\*[[:space:]]*//')
|
|
95
135
|
git fetch origin --prune --quiet
|
|
96
136
|
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
137
|
```
|
|
98
138
|
|
|
99
|
-
**
|
|
139
|
+
**17. P5 — Frozen ticket Status post-merge.** Multi-word status via sed char class, not `\w+`.
|
|
100
140
|
```bash
|
|
101
141
|
FROZEN_COUNT=0
|
|
102
142
|
for t in docs/tickets/*.md; do
|
|
@@ -114,30 +154,49 @@ done
|
|
|
114
154
|
[ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
|
|
115
155
|
```
|
|
116
156
|
|
|
117
|
-
**
|
|
157
|
+
**18. P6 — AC count off-by-N (header-form aware since v0.19.0).** Supports two AC forms: `[x]`/`[ ]` checkbox form and `### AC<N>` header form. Headers authoritative when present (≥ 1). A header is "deferred" when its body contains `**Status**: Deferred|Pending|Skipped|Blocked` before the next `### `. Two claim shapes: `all N marked` and `AC: X/Y done`. v0.19.0 drops the v0.18.3 `-ge 2` tolerance — exact-match (off-by-1 advisories surface).
|
|
118
158
|
```bash
|
|
119
159
|
AC_BLOCK=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
HEADER_ACS=$(echo "$AC_BLOCK" | grep -cE '^### AC[0-9]+')
|
|
161
|
+
|
|
162
|
+
if [ "$HEADER_ACS" -gt 0 ]; then
|
|
163
|
+
ACTUAL_TOTAL=$HEADER_ACS
|
|
164
|
+
# Per-AC pass: emit one line per AC ("marked" or "deferred"). Then count.
|
|
165
|
+
PER_AC=$(echo "$AC_BLOCK" | awk '
|
|
166
|
+
function flush() { if (have) { print (deferred ? "deferred" : "marked"); have=0; deferred=0 } }
|
|
167
|
+
/^### AC[0-9]+/ { flush(); have=1; next }
|
|
168
|
+
/^## / { flush(); next }
|
|
169
|
+
have && /^\*\*Status\*\*:[[:space:]]*(Deferred|Pending|Skipped|Blocked)/ { deferred=1 }
|
|
170
|
+
END { flush() }
|
|
171
|
+
')
|
|
172
|
+
ACTUAL_MARKED=$(printf '%s\n' "$PER_AC" | grep -c '^marked$' || true)
|
|
173
|
+
else
|
|
174
|
+
ACTUAL_TOTAL=$(echo "$AC_BLOCK" | grep -cE "^- \[[x ]\]")
|
|
175
|
+
ACTUAL_MARKED=$(echo "$AC_BLOCK" | grep -cE "^- \[x\]")
|
|
176
|
+
fi
|
|
177
|
+
|
|
122
178
|
CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
|
|
123
179
|
if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
|
|
124
180
|
CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
|
|
125
181
|
CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
|
|
126
182
|
[ -n "$CLAIMED_TOTAL" ] && [ "$CLAIMED_TOTAL" != "$ACTUAL_TOTAL" ] \
|
|
127
|
-
&&
|
|
128
|
-
&& flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL"
|
|
183
|
+
&& flag "P6 drift: claim AC total '$CLAIMED_TOTAL' vs actual total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
129
184
|
[ -n "$CLAIMED_MARKED" ] && [ "$CLAIMED_MARKED" != "$ACTUAL_MARKED" ] \
|
|
130
|
-
&&
|
|
131
|
-
&& flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED"
|
|
185
|
+
&& flag "P6 drift: claim AC marked '$CLAIMED_MARKED' vs actual marked $ACTUAL_MARKED (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
132
186
|
elif [ -n "$CLAIM_LINE" ]; then
|
|
133
187
|
CLAIMED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
|
|
134
|
-
[ -n "$CLAIMED" ]
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
if [ -n "$CLAIMED" ]; then
|
|
189
|
+
[ "$CLAIMED" != "$ACTUAL_TOTAL" ] \
|
|
190
|
+
&& flag "P6 drift: 'all $CLAIMED marked' vs actual AC total $ACTUAL_TOTAL (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
191
|
+
# `all N marked` IMPLIES all ACs are marked; flag when actual marked < N
|
|
192
|
+
# (e.g. some ACs deferred via `**Status**: Deferred` or unchecked `[ ]`).
|
|
193
|
+
[ "$CLAIMED" != "$ACTUAL_MARKED" ] \
|
|
194
|
+
&& flag "P6 drift: 'all $CLAIMED marked' but only $ACTUAL_MARKED actually marked (form: $([ "$HEADER_ACS" -gt 0 ] && echo header || echo checkbox))"
|
|
195
|
+
fi
|
|
137
196
|
fi
|
|
138
197
|
```
|
|
139
198
|
|
|
140
|
-
**
|
|
199
|
+
**19. P7 — Test count drift within ticket (final-sections only).**
|
|
141
200
|
```bash
|
|
142
201
|
TERMINAL=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET" | grep -iE "(test|pass|green)" | grep -oE "[0-9]+/[0-9]+" | tail -1)
|
|
143
202
|
AC=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
|
|
@@ -148,7 +207,7 @@ for n in $FINAL_NUMS; do
|
|
|
148
207
|
done
|
|
149
208
|
```
|
|
150
209
|
|
|
151
|
-
**
|
|
210
|
+
**20. P8 — Completion Log gap vs Workflow Checklist.**
|
|
152
211
|
```bash
|
|
153
212
|
WORKFLOW=$(awk '/^## Workflow Checklist/,/^## Completion Log/' "$TICKET")
|
|
154
213
|
COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
@@ -159,7 +218,7 @@ while read -r step_num; do
|
|
|
159
218
|
done <<< "$CHECKED_STEPS"
|
|
160
219
|
```
|
|
161
220
|
|
|
162
|
-
**
|
|
221
|
+
**21. P9 — Tracker header stale.**
|
|
163
222
|
```bash
|
|
164
223
|
TRACKER=docs/project_notes/product-tracker.md
|
|
165
224
|
HEADER_STEP=$(grep '^\*\*Last Updated:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
@@ -168,7 +227,7 @@ DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE '(Step
|
|
|
168
227
|
&& flag "P9 drift: tracker header says $HEADER_STEP, Active Feature says $DETAIL_STEP"
|
|
169
228
|
```
|
|
170
229
|
|
|
171
|
-
**
|
|
230
|
+
**22. P10 — Duplicate Completion Log rows.**
|
|
172
231
|
```bash
|
|
173
232
|
awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
174
233
|
key = $2 "|" $3 "|" substr($4, 1, 80)
|
|
@@ -178,7 +237,7 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
|
178
237
|
| while read -r dup; do flag "P10 drift: duplicate Completion Log row: $dup"; done
|
|
179
238
|
```
|
|
180
239
|
|
|
181
|
-
**
|
|
240
|
+
**23. P11 — Tracker Features table status vs ticket Status mismatch.**
|
|
182
241
|
```bash
|
|
183
242
|
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
184
243
|
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
@@ -186,18 +245,36 @@ TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
|
186
245
|
| sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
|
|
187
246
|
| sed -E 's/\*\*[[:space:]]*$//' \
|
|
188
247
|
| sed -E 's/[[:space:]]+$//')
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
TICKET_BASENAME=$(basename "$TICKET" .md)
|
|
249
|
+
# v0.18.4 P11-B: sub-scope tickets (-lite / -FU / -FU[0-9]*) close a partial
|
|
250
|
+
# scope of a parent feature; the parent tracker row stays at its parent status
|
|
251
|
+
# (typically `pending` or `in-progress`) while the sub-scope ticket reaches
|
|
252
|
+
# `Done`. P11 must NOT enforce status mapping across this boundary. Pattern
|
|
253
|
+
# scope: empirically derived from fx convention (uppercase -FU + -lite). If
|
|
254
|
+
# future projects introduce -spike / -mini / -aux variants, expand here.
|
|
255
|
+
case "$TICKET_BASENAME" in
|
|
256
|
+
*-lite|*-lite-*|*-FU|*-FU-*|*-FU[0-9]*)
|
|
257
|
+
echo "P11 N/A: $TICKET_BASENAME is a sub-scope ticket — parent tracker row status independent" >&2
|
|
258
|
+
;;
|
|
259
|
+
*)
|
|
260
|
+
FEATURE_ID=$(echo "$TICKET_BASENAME" | sed -E 's/-[a-z].*//')
|
|
261
|
+
# v0.18.4 P11-B drive-by hardening: anchor tracker lookup to pipe-table row
|
|
262
|
+
# (FEATURE_ID as first cell). Mirrors v0.18.3 P16 idiom — prevents narrative
|
|
263
|
+
# mentions earlier in the tracker from silencing or false-firing the check.
|
|
264
|
+
TRACKER_STATUS=$(grep -E "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" docs/project_notes/product-tracker.md \
|
|
265
|
+
| grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
|
|
266
|
+
case "$TICKET_STATUS" in
|
|
267
|
+
"Ready for Merge"|"Review"|"In Progress"|"Planning"|"Spec") EXPECTED="in-progress" ;;
|
|
268
|
+
"Done") EXPECTED="done" ;;
|
|
269
|
+
*) EXPECTED="" ;;
|
|
270
|
+
esac
|
|
271
|
+
[ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
|
|
272
|
+
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
273
|
+
;;
|
|
195
274
|
esac
|
|
196
|
-
[ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
|
|
197
|
-
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
198
275
|
```
|
|
199
276
|
|
|
200
|
-
**
|
|
277
|
+
**24. 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.
|
|
201
278
|
```bash
|
|
202
279
|
TRACKER=docs/project_notes/product-tracker.md
|
|
203
280
|
if [ -f "$TRACKER" ]; then
|
|
@@ -217,7 +294,7 @@ if [ -f "$TRACKER" ]; then
|
|
|
217
294
|
fi
|
|
218
295
|
```
|
|
219
296
|
|
|
220
|
-
**
|
|
297
|
+
**25. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** Whitespace-safe iteration + FEATURE_ID anchoring.
|
|
221
298
|
```bash
|
|
222
299
|
KEY_FACTS=docs/project_notes/key_facts.md
|
|
223
300
|
if [ -f "$KEY_FACTS" ]; then
|
|
@@ -233,7 +310,7 @@ if [ -f "$KEY_FACTS" ]; then
|
|
|
233
310
|
fi
|
|
234
311
|
```
|
|
235
312
|
|
|
236
|
-
**
|
|
313
|
+
**26. P14 — MCE Action 1 row stale post-merge (added v0.18.3).** Strict awk state machine terminates MCE block at NEXT `^## ` of any name (not `[^M]`). Strict signal `Step 6 [ ]` / `Step 6 [-]` only — standalone `(this merge)` omitted to prevent false positives on past-tense narrative. Reuses `TICKET_STATUS` from P11. NIT severity.
|
|
237
314
|
```bash
|
|
238
315
|
if [ "$TICKET_STATUS" = "Done" ]; then
|
|
239
316
|
MCE_BLOCK=$(awk '
|
|
@@ -247,7 +324,7 @@ if [ "$TICKET_STATUS" = "Done" ]; then
|
|
|
247
324
|
fi
|
|
248
325
|
```
|
|
249
326
|
|
|
250
|
-
**
|
|
327
|
+
**27. P15 — AC with post-deploy keyword admitted without Completion Log evidence (added v0.18.3).** Line-safe iteration via `while IFS= read -r`.
|
|
251
328
|
```bash
|
|
252
329
|
COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
253
330
|
AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
|
|
@@ -263,7 +340,7 @@ while IFS= read -r line; do
|
|
|
263
340
|
done <<< "$AC_LINES"
|
|
264
341
|
```
|
|
265
342
|
|
|
266
|
-
**
|
|
343
|
+
**28. P16 — Feature missing from Features table (added v0.18.3).** Strict signal: feature ID must appear as first cell of a pipe-table row (`| FEATURE_ID |`) — narrative mentions / `**Active Feature:**` references must not silence the drift. NIT severity. Reuses `TICKET_STATUS` and `FEATURE_ID` from P11.
|
|
267
344
|
```bash
|
|
268
345
|
TRACKER=docs/project_notes/product-tracker.md
|
|
269
346
|
case "$TICKET_STATUS" in
|
|
@@ -298,7 +375,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
298
375
|
```
|
|
299
376
|
## Merge Compliance Audit — [FEATURE-ID]
|
|
300
377
|
|
|
301
|
-
### Structural (1-
|
|
378
|
+
### Structural (1-12) — blocking merge gate
|
|
302
379
|
|
|
303
380
|
| # | Check | Status | Detail |
|
|
304
381
|
|---|-------|:------:|--------|
|
|
@@ -313,39 +390,48 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
313
390
|
| 9 | Merge Base | PASS | Up to date with develop |
|
|
314
391
|
| 10 | Working Tree | PASS | Clean |
|
|
315
392
|
| 11 | Data Files | PASS | N/A — no JSON seed files |
|
|
393
|
+
| 12 | CI State | PASS | PR #123 — all checks pass or pending |
|
|
316
394
|
|
|
317
395
|
**STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
|
|
318
396
|
|
|
319
|
-
### Drift (
|
|
397
|
+
### Drift (13-28) — advisory, refresh before user authorization
|
|
320
398
|
|
|
321
399
|
| # | Pattern | Status | Detail |
|
|
322
400
|
|---|---------|:------:|--------|
|
|
323
|
-
|
|
|
324
|
-
|
|
|
325
|
-
|
|
|
326
|
-
|
|
|
327
|
-
|
|
|
328
|
-
|
|
|
329
|
-
|
|
|
330
|
-
|
|
|
331
|
-
|
|
|
332
|
-
|
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
401
|
+
| 13 | P1 PR body test count stale | PASS | matches ticket terminal |
|
|
402
|
+
| 14 | P2 Aspirational Evidence rows | PASS | all past-tense |
|
|
403
|
+
| 15 | P3 Post-merge actions logged | PASS | N/A pre-merge |
|
|
404
|
+
| 16 | P4 Remote branch orphan | PASS | not checked pre-merge |
|
|
405
|
+
| 17 | P5 Frozen ticket Status | PASS | 0 frozen |
|
|
406
|
+
| 18 | P6 AC count off-by-N | PASS | claim matches actual (form: header) |
|
|
407
|
+
| 19 | P7 Intra-ticket test drift | PASS | final = terminal |
|
|
408
|
+
| 20 | P8 Completion Log gap | PASS | every [x] step has narrative |
|
|
409
|
+
| 21 | P9 Tracker header stale | PASS | header = detail |
|
|
410
|
+
| 22 | P10 Duplicate log rows | PASS | no duplicates |
|
|
411
|
+
| 23 | P11 Tracker status mismatch | PASS | status consistent |
|
|
412
|
+
| 24 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
|
|
413
|
+
| 25 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
|
|
414
|
+
| 26 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
|
|
415
|
+
| 27 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
|
|
416
|
+
| 28 | P16 Feature missing from tracker | PASS | feature in Features table |
|
|
339
417
|
|
|
340
418
|
**DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
|
|
341
419
|
|
|
342
420
|
### Combined verdict
|
|
343
421
|
|
|
344
|
-
- Both PASS → **READY FOR MERGE**
|
|
422
|
+
- Both PASS → **READY FOR MERGE** (compliance 12/12, drift clean)
|
|
345
423
|
- Structural fail → **NEEDS FIX — N blockers**
|
|
346
424
|
- Structural pass + drift advisories → **READY FOR MERGE PENDING DRIFT CLEANUP — N advisories**
|
|
347
425
|
```
|
|
348
426
|
|
|
427
|
+
**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:
|
|
428
|
+
- Numeric ratios (`N/M`): preserve as emitted. `18/19` MUST NOT become `18/18` even when N < M.
|
|
429
|
+
- 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.
|
|
430
|
+
- "Form" annotations (e.g. P6 emits `(form: header)` or `(form: checkbox)`): preserve them so the reader can tell which AC form the ticket used.
|
|
431
|
+
- `N/A` reasons: quote the literal reason emitted by the recipe (`no PR open`, `gh CLI unavailable`, `jq unavailable`, `not checked pre-merge`).
|
|
432
|
+
|
|
433
|
+
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.
|
|
434
|
+
|
|
349
435
|
### If issues are found
|
|
350
436
|
|
|
351
437
|
Fix them directly:
|
|
@@ -354,8 +440,9 @@ Fix them directly:
|
|
|
354
440
|
- Tracker stale → update Active Session and Features table
|
|
355
441
|
- Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
|
|
356
442
|
- Data file issues → fix the data
|
|
443
|
+
- CI failures (C3) → re-run failed jobs after addressing root cause; do NOT merge while `ci-success` is BLOCKED
|
|
357
444
|
|
|
358
|
-
**Drift advisories (
|
|
445
|
+
**Drift advisories (13-28) fixes:**
|
|
359
446
|
- **P1** → edit PR body npm test line to match ticket terminal count
|
|
360
447
|
- **P2** → rewrite `[x]` rows with past-tense + commit SHA
|
|
361
448
|
- **P3** → add Completion Log row for each post-merge execution
|
|
@@ -74,6 +74,14 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
|
|
|
74
74
|
|
|
75
75
|
**Canonical form for the AC count claim:** write `AC: <marked>/<total>` — `marked` is the count of `[x]` Acceptance Criteria, `total` is the count of all AC items including any intentionally deferred `[ ]`. When all are checked use the matching form `AC: N/N` (or the shorthand `all N marked`). The `/audit-merge` P6 drift check parses both forms.
|
|
76
76
|
|
|
77
|
+
**Sub-scope ticket naming convention (recognized by `/audit-merge` P11 since v0.18.4):** when a feature is too large to close in one ticket, split it into sub-scope tickets using one of these suffixes on the basename:
|
|
78
|
+
|
|
79
|
+
- `<FEATURE_ID>-lite-<descriptor>.md` — minimal viable closure of a partial scope (e.g. `F116-lite-ci-hardening.md`)
|
|
80
|
+
- `<FEATURE_ID>-FU.md` — single follow-up closing a deferred piece
|
|
81
|
+
- `<FEATURE_ID>-FU<N>.md` — numbered follow-ups (e.g. `F-H7-FU1.md`, `F-H10-FU2.md`)
|
|
82
|
+
|
|
83
|
+
Sub-scope tickets reach `Status: Done` independently while the parent feature's tracker row stays at its parent status (typically `pending` or `in-progress`) until ALL sub-scopes close. `/audit-merge` P11 detects the suffix and emits `P11 N/A` instead of flagging the parent/sub-scope status divergence as drift.
|
|
84
|
+
|
|
77
85
|
## Action 9: Run compliance audit
|
|
78
86
|
|
|
79
87
|
Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.
|