create-sdd-project 0.18.1 → 0.18.3

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.
@@ -91,6 +91,13 @@ const WORKFLOW_CORE_PROJECT_TYPE_RULES = {
91
91
  [/### UI Changes \(if applicable\)\n\n\[Components to add\/modify\. Reference `docs\/specs\/ui-components\.md`\.\]\n\n/, ''],
92
92
  [' / `ui-components.md`', ''],
93
93
  ],
94
+ // v0.18.2 (Codex R1 finding C4): pr-template.md is project-type adapted —
95
+ // moved from inline replaceInFileFn to this table so smart-diff fallback
96
+ // compare via adaptWorkflowCoreContentForProjectType produces a matching
97
+ // pristine target on backend/frontend installs (no false-preserve).
98
+ 'skills/development-workflow/references/pr-template.md': [
99
+ [' / ui-components.md', ''],
100
+ ],
94
101
  },
95
102
  frontend: {
96
103
  'skills/development-workflow/SKILL.md': [
@@ -101,6 +108,9 @@ const WORKFLOW_CORE_PROJECT_TYPE_RULES = {
101
108
  [/### API Changes \(if applicable\)\n\n\[Endpoints to add\/modify\. Reference[^\]]*\]\n\n/, ''],
102
109
  ['`api-spec.yaml` / ', ''],
103
110
  ],
111
+ 'skills/development-workflow/references/pr-template.md': [
112
+ ['api-spec.yaml / ', ''],
113
+ ],
104
114
  },
105
115
  };
106
116
 
@@ -243,25 +253,16 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
243
253
  replaceInFileFn(path.join(dest, 'AGENTS.md'), [
244
254
  [', `ui-ux-designer`', ''],
245
255
  ]);
246
- for (const dir of toolDirs) {
247
- // pr-template: remove ui-components from checklist (out of scope for
248
- // v0.17.1 see dev/ROADMAP.md "Known follow-ups" item 2)
249
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
250
- [' / ui-components.md', ''],
251
- ]);
252
- }
256
+ // v0.18.2 (Codex R1 C4): pr-template.md rules moved to WORKFLOW_CORE_PROJECT_TYPE_RULES
257
+ // (handled by the wfRules loop above) so smart-diff fallback compare can
258
+ // produce a pristine adapted target via adaptWorkflowCoreContentForProjectType.
253
259
  // base-standards.mdc: remove ui-ux-designer table row (shared table above)
254
260
  replaceInFileFn(
255
261
  path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc'),
256
262
  BASE_STANDARDS_PROJECT_TYPE_RULES.backend
257
263
  );
258
- } else if (config.projectType === 'frontend') {
259
- for (const dir of toolDirs) {
260
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
261
- ['api-spec.yaml / ', ''],
262
- ]);
263
- }
264
264
  }
265
+ // v0.18.2: frontend pr-template rule also moved to WORKFLOW_CORE_PROJECT_TYPE_RULES.
265
266
  }
266
267
 
267
268
  module.exports = {
package/lib/meta.js CHANGED
@@ -273,14 +273,41 @@ function expectedSmartDiffTrackedPaths(aiTools, projectType) {
273
273
  paths.add('ai-specs/specs/documentation-standards.mdc');
274
274
 
275
275
  // v0.17.1: development-workflow skill core files — filtered by aiTools.
276
- // bug-workflow, health-check, pm-orchestrator, project-memory are OUT OF
277
- // SCOPE for v0.17.1 (deferred — see dev/ROADMAP.md "Known follow-ups" item 2).
278
276
  for (const dir of toolDirs) {
279
277
  paths.add(`${dir}/skills/development-workflow/SKILL.md`);
280
278
  paths.add(`${dir}/skills/development-workflow/references/ticket-template.md`);
281
279
  paths.add(`${dir}/skills/development-workflow/references/merge-checklist.md`);
282
280
  }
283
281
 
282
+ // v0.18.2: extend smart-diff coverage to remaining SKILL.md entry points +
283
+ // 11 reference files (closes ROADMAP "Known follow-ups" item 1 — long-deferred
284
+ // since v0.17.1). Universal per aiTools (no project-type filtering — skills
285
+ // and reference templates are stack-agnostic except pr-template.md, which
286
+ // has project-type rules in WORKFLOW_CORE_PROJECT_TYPE_RULES so the
287
+ // fallback compare in upgrade-generator.js produces a pristine adapted
288
+ // target).
289
+ for (const dir of toolDirs) {
290
+ // 4 SKILL.md entry points
291
+ paths.add(`${dir}/skills/bug-workflow/SKILL.md`);
292
+ paths.add(`${dir}/skills/health-check/SKILL.md`);
293
+ paths.add(`${dir}/skills/pm-orchestrator/SKILL.md`);
294
+ paths.add(`${dir}/skills/project-memory/SKILL.md`);
295
+ // 7 development-workflow references
296
+ paths.add(`${dir}/skills/development-workflow/references/pr-template.md`);
297
+ paths.add(`${dir}/skills/development-workflow/references/branching-strategy.md`);
298
+ paths.add(`${dir}/skills/development-workflow/references/failure-handling.md`);
299
+ paths.add(`${dir}/skills/development-workflow/references/workflow-example.md`);
300
+ paths.add(`${dir}/skills/development-workflow/references/complexity-guide.md`);
301
+ paths.add(`${dir}/skills/development-workflow/references/add-feature-template.md`);
302
+ paths.add(`${dir}/skills/development-workflow/references/cross-model-review.md`);
303
+ // 1 pm-orchestrator reference
304
+ paths.add(`${dir}/skills/pm-orchestrator/references/pm-session-template.md`);
305
+ // 3 project-memory references
306
+ paths.add(`${dir}/skills/project-memory/references/bugs_template.md`);
307
+ paths.add(`${dir}/skills/project-memory/references/decisions_template.md`);
308
+ paths.add(`${dir}/skills/project-memory/references/key_facts_template.md`);
309
+ }
310
+
284
311
  // v0.18.1: shipped slash-commands — preserve user customizations across
285
312
  // upgrades. Closes v0.18.0 known limitation where audit-merge.md was
286
313
  // wholesale-overwritten (notable since teams may tune drift recipes for
@@ -305,21 +305,45 @@ function generateUpgrade(config) {
305
305
  const filesToAdapt = new Set();
306
306
 
307
307
  // v0.17.1: before the skills/ wholesale delete-and-copy, save the content
308
- // of the 6 workflow-core files so we can restore them if their hash tells
308
+ // of the workflow-core files so we can restore them if their hash tells
309
309
  // us they were customized. Map keyed by absolute path, value = string or
310
310
  // null (null = file didn't exist before upgrade).
311
+ // v0.18.2: extended from 6 paths to 21 paths × tool dirs (max 42 for
312
+ // fullstack-both). Adds 4 SKILL.md entry points + 11 reference files,
313
+ // closing ROADMAP "Known follow-ups" item 1.
311
314
  const workflowCoreBackup = new Map();
312
315
  const workflowCorePosixPaths = [];
313
316
  {
314
317
  const dirsForBackup = [];
315
318
  if (aiTools !== 'gemini') dirsForBackup.push('.claude');
316
319
  if (aiTools !== 'claude') dirsForBackup.push('.gemini');
320
+ const WORKFLOW_CORE_RELATIVE_PATHS = [
321
+ // v0.17.1 (3 files)
322
+ 'skills/development-workflow/SKILL.md',
323
+ 'skills/development-workflow/references/ticket-template.md',
324
+ 'skills/development-workflow/references/merge-checklist.md',
325
+ // v0.18.2: 4 SKILL.md entry points
326
+ 'skills/bug-workflow/SKILL.md',
327
+ 'skills/health-check/SKILL.md',
328
+ 'skills/pm-orchestrator/SKILL.md',
329
+ 'skills/project-memory/SKILL.md',
330
+ // v0.18.2: 7 development-workflow references
331
+ 'skills/development-workflow/references/pr-template.md',
332
+ 'skills/development-workflow/references/branching-strategy.md',
333
+ 'skills/development-workflow/references/failure-handling.md',
334
+ 'skills/development-workflow/references/workflow-example.md',
335
+ 'skills/development-workflow/references/complexity-guide.md',
336
+ 'skills/development-workflow/references/add-feature-template.md',
337
+ 'skills/development-workflow/references/cross-model-review.md',
338
+ // v0.18.2: 1 pm-orchestrator reference
339
+ 'skills/pm-orchestrator/references/pm-session-template.md',
340
+ // v0.18.2: 3 project-memory references
341
+ 'skills/project-memory/references/bugs_template.md',
342
+ 'skills/project-memory/references/decisions_template.md',
343
+ 'skills/project-memory/references/key_facts_template.md',
344
+ ];
317
345
  for (const dir of dirsForBackup) {
318
- for (const relSub of [
319
- 'skills/development-workflow/SKILL.md',
320
- 'skills/development-workflow/references/ticket-template.md',
321
- 'skills/development-workflow/references/merge-checklist.md',
322
- ]) {
346
+ for (const relSub of WORKFLOW_CORE_RELATIVE_PATHS) {
323
347
  const posix = `${dir}/${relSub}`;
324
348
  const abs = path.join(dest, ...posix.split('/'));
325
349
  workflowCorePosixPaths.push({ posix, abs });
@@ -370,13 +394,13 @@ function generateUpgrade(config) {
370
394
  fs.rmSync(subDir, { recursive: true, force: true });
371
395
  }
372
396
  }
373
- // For .gemini, also remove commands (SDD-owned Gemini TOML commands)
374
- if (dir === '.gemini') {
375
- const cmdDir = path.join(base, 'commands');
376
- if (fs.existsSync(cmdDir)) {
377
- fs.rmSync(cmdDir, { recursive: true, force: true });
378
- }
379
- }
397
+ // v0.18.2: do NOT wholesale-delete .gemini/commands. The smart-diff loop
398
+ // below iterates template command files individually and preserves user
399
+ // customizations of tracked paths (e.g. audit-merge.toml) — closing the
400
+ // v0.18.1 plumbing gap where these files were silently overwritten on
401
+ // every upgrade despite being tracked in expectedSmartDiffTrackedPaths.
402
+ // Custom Gemini commands (files NOT in the template) are left in place,
403
+ // mirroring the agent strategy from v0.16.10.
380
404
 
381
405
  // Copy fresh from template
382
406
  const templateToolDir = path.join(templateDir, dir);
@@ -522,16 +546,117 @@ function generateUpgrade(config) {
522
546
  continue;
523
547
  }
524
548
 
525
- // For .claude/commands, merge: overwrite SDD template commands, preserve user's custom commands
526
- if (dir === '.claude' && sub === 'commands') {
549
+ // v0.18.2: smart-diff loop for commands (closes v0.18.1 plumbing gap).
550
+ //
551
+ // Pre-v0.18.2: .claude/commands wholesale cpSync'd every file (line 525-531)
552
+ // and .gemini/commands wholesale rmSync'd + recursive cpSync'd (lines 374-378
553
+ // and the fallback at 532-533). Both branches overwrote user customizations
554
+ // silently despite the paths being added to expectedSmartDiffTrackedPaths in
555
+ // v0.18.1 (lib/meta.js:284-320).
556
+ //
557
+ // Now: iterate template command files individually. For each file:
558
+ // - If posix path is tracked → smart-diff decision tree (preserve customizations)
559
+ // - Else → unconditional overwrite (SDD-owned wrappers, e.g. add-feature.toml)
560
+ // Custom commands (files NOT in template) are NOT touched, mirroring the
561
+ // agent strategy from v0.16.10.
562
+ //
563
+ // Commands have NO project-type or stack adaptations — adapted target == raw template.
564
+ if (sub === 'commands') {
527
565
  fs.mkdirSync(destSub, { recursive: true });
528
- for (const file of fs.readdirSync(srcSub)) {
529
- // Always overwrite template-owned files (they may have been updated)
530
- fs.cpSync(path.join(srcSub, file), path.join(destSub, file));
566
+ const trackedSet = expectedSmartDiffTrackedPaths(aiTools, projectType);
567
+ const templateCmdFiles = fs
568
+ .readdirSync(srcSub, { withFileTypes: true })
569
+ .filter((e) => e.isFile())
570
+ .map((e) => e.name);
571
+
572
+ for (const file of templateCmdFiles) {
573
+ const templateCmdPath = path.join(srcSub, file);
574
+ const existingCmdPath = path.join(destSub, file);
575
+ const relativePath = path.relative(dest, existingCmdPath);
576
+ const posixPath = toPosix(relativePath);
577
+ const rawTemplate = fs.readFileSync(templateCmdPath, 'utf8');
578
+
579
+ if (!trackedSet.has(posixPath)) {
580
+ // SDD-owned (e.g. add-feature.toml, fix-bug.toml, next-task.toml,
581
+ // show-progress.toml, start-task.toml) — unconditional overwrite.
582
+ if (fs.existsSync(existingCmdPath)) {
583
+ backupBeforeReplace(dest, relativePath, backupTimestamp);
584
+ }
585
+ fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
586
+ replaced++;
587
+ continue;
588
+ }
589
+
590
+ // --- v0.18.2 commands smart-diff decision tree (mirror of agents) ---
591
+ if (!fs.existsSync(existingCmdPath)) {
592
+ fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
593
+ filesToAdapt.add(posixPath);
594
+ replaced++;
595
+ continue;
596
+ }
597
+
598
+ if (config.forceTemplate) {
599
+ backupBeforeReplace(dest, relativePath, backupTimestamp);
600
+ fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
601
+ filesToAdapt.add(posixPath);
602
+ replaced++;
603
+ continue;
604
+ }
605
+
606
+ const existingContent = fs.readFileSync(existingCmdPath, 'utf8');
607
+ const storedHash = meta && meta.hashes[posixPath];
608
+
609
+ const preserveCmd = () => {
610
+ backupBeforeReplace(dest, relativePath, backupTimestamp);
611
+ const newBackupPath = path.join(
612
+ dest,
613
+ '.sdd-backup',
614
+ backupTimestamp,
615
+ `${relativePath}.new`
616
+ );
617
+ try {
618
+ fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
619
+ fs.writeFileSync(newBackupPath, rawTemplate, 'utf8');
620
+ } catch (e) {
621
+ console.warn(
622
+ ` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`
623
+ );
624
+ }
625
+ modifiedAgentsResults.push({ name: relativePath, modified: true });
626
+ preserved++;
627
+ // Codex M1 invariant: preserved files do NOT update newHashes.
628
+ };
629
+
630
+ if (storedHash) {
631
+ const currentHash = computeHash(existingContent);
632
+ if (currentHash === storedHash) {
633
+ // Pristine — replace + record new hash.
634
+ backupBeforeReplace(dest, relativePath, backupTimestamp);
635
+ fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
636
+ filesToAdapt.add(posixPath);
637
+ replaced++;
638
+ continue;
639
+ }
640
+ // Hash mismatch → user customized → preserve.
641
+ preserveCmd();
642
+ continue;
643
+ }
644
+
645
+ // Fallback — no hash (e.g. pre-v0.18.1 install). Compare against
646
+ // raw template (commands have no adapters).
647
+ if (normalizedContentEquals(existingContent, rawTemplate)) {
648
+ backupBeforeReplace(dest, relativePath, backupTimestamp);
649
+ fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
650
+ filesToAdapt.add(posixPath);
651
+ replaced++;
652
+ continue;
653
+ }
654
+ preserveCmd();
531
655
  }
532
- } else {
533
- fs.cpSync(srcSub, destSub, { recursive: true });
656
+ continue;
534
657
  }
658
+
659
+ fs.cpSync(srcSub, destSub, { recursive: true });
535
660
  replaced++;
536
661
  }
537
662
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
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"
@@ -52,12 +52,21 @@ Run only if `git diff origin/<target-branch>..HEAD --name-only` shows `.json` fi
52
52
 
53
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`).
54
54
 
55
- **12. P1 — PR body test count stale.** The PR body's "npm test" line should match the terminal test count in the ticket (AC / DoD / Completion Log last entry). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body number becomes stale.
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.
56
56
  ```bash
57
- PR_BODY=$(gh pr view --json body -q .body)
58
- PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
59
- TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
60
- [ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
57
+ TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
58
+ PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
59
+ PR_RATIOS=$(echo "$PR_BODY" | grep -iE "$TEST_KW_RE" | grep -oE "[0-9]+/[0-9]+" | sort -u)
60
+ TICKET_RATIOS=$(grep -iE "$TEST_KW_RE" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | sort -u)
61
+ if [ -z "$PR_RATIOS" ] || [ -z "$TICKET_RATIOS" ]; then
62
+ echo "P1 N/A: no comparable test-count ratios extracted (PR=$(echo "$PR_RATIOS" | tr '\n' ',' ), ticket=$(echo "$TICKET_RATIOS" | tr '\n' ',' ))"
63
+ else
64
+ while IFS= read -r r; do
65
+ [ -z "$r" ] && continue
66
+ echo "$TICKET_RATIOS" | grep -qFx "$r" \
67
+ || flag "P1 drift: PR ratio $r not found in ticket evidence (ticket ratios: $(echo "$TICKET_RATIOS" | tr '\n' ' '))"
68
+ done <<< "$PR_RATIOS"
69
+ fi
61
70
  ```
62
71
 
63
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.
@@ -94,6 +103,8 @@ for t in docs/tickets/*.md; do
94
103
  status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
95
104
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
96
105
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
106
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
107
+ | sed -E 's/\*\*[[:space:]]*$//' \
97
108
  | sed -E 's/[[:space:]]+$//')
98
109
  [ "$status" = "Done" ] && continue
99
110
  ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
@@ -103,12 +114,27 @@ done
103
114
  [ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
104
115
  ```
105
116
 
106
- **17. P6 — AC count off-by-N.** Merge Checklist Evidence row 1 claim ("all N marked" / "AC: X/Y") diverges from actual count of `[x]` + `[ ]` in `## Acceptance Criteria`.
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.
107
118
  ```bash
108
- ACTUAL=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET" | grep -cE "^- \[[x ]\]")
109
- CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | grep -oE "[0-9]+" | head -1)
110
- [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL" ] && [ $((ACTUAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL)) -ge 2 ] \
111
- && flag "P6 drift: claim '$CLAIMED' vs actual AC count $ACTUAL"
119
+ 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\]")
122
+ CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
123
+ if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
124
+ CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
125
+ CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
126
+ [ -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"
129
+ [ -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"
132
+ elif [ -n "$CLAIM_LINE" ]; then
133
+ 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"
137
+ fi
112
138
  ```
113
139
 
114
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.
@@ -157,6 +183,8 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
157
183
  TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
158
184
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
159
185
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
186
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
187
+ | sed -E 's/\*\*[[:space:]]*$//' \
160
188
  | sed -E 's/[[:space:]]+$//')
161
189
  FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
162
190
  TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
@@ -169,9 +197,89 @@ esac
169
197
  && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
170
198
  ```
171
199
 
200
+ **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.
201
+ ```bash
202
+ TRACKER=docs/project_notes/product-tracker.md
203
+ if [ -f "$TRACKER" ]; then
204
+ ACTUAL_HEAD=$(git rev-parse HEAD 2>/dev/null || true)
205
+ if [ -n "$ACTUAL_HEAD" ]; then
206
+ ACTUAL_SHORT=$(printf '%s' "$ACTUAL_HEAD" | cut -c1-7)
207
+ TRACKER_HEADS=$(grep -E '^\*\*(Last Updated|Active Feature):\*\*' "$TRACKER" 2>/dev/null \
208
+ | grep -oE 'HEAD[[:space:]:]+`?[a-f0-9]{7,40}`?' \
209
+ | grep -oE '[a-f0-9]{7,40}' \
210
+ | sort -u || true)
211
+ for sha in $TRACKER_HEADS; do
212
+ case "$ACTUAL_HEAD" in "$sha"*) continue ;; esac
213
+ case "$sha" in "$ACTUAL_SHORT"*) continue ;; esac
214
+ flag "P12 drift: tracker HEAD reference $sha does not match git rev-parse HEAD ($ACTUAL_HEAD); refresh tracker"
215
+ done
216
+ fi
217
+ fi
218
+ ```
219
+
220
+ **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.
221
+ ```bash
222
+ KEY_FACTS=docs/project_notes/key_facts.md
223
+ if [ -f "$KEY_FACTS" ]; then
224
+ FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
225
+ TICKET_DELTAS=$(grep -oE '\+[0-9]+ (atoms?|aliases?|dishes?|entries|rows)' "$TICKET" | sort -u)
226
+ KEY_FACTS_BLOCK=$(grep -A 3 -F "$FEATURE_ID" "$KEY_FACTS" 2>/dev/null || true)
227
+ while IFS= read -r claim; do
228
+ [ -z "$claim" ] && continue
229
+ if [ -z "$KEY_FACTS_BLOCK" ] || ! echo "$KEY_FACTS_BLOCK" | grep -qF "$claim"; then
230
+ flag "P13 drift: ticket claims '$claim' but key_facts.md block for $FEATURE_ID lacks the same delta"
231
+ fi
232
+ done <<< "$TICKET_DELTAS"
233
+ fi
234
+ ```
235
+
236
+ **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.
237
+ ```bash
238
+ if [ "$TICKET_STATUS" = "Done" ]; then
239
+ MCE_BLOCK=$(awk '
240
+ /^## Merge Checklist Evidence/ { in_mce=1; print; next }
241
+ in_mce && /^## / { in_mce=0 }
242
+ in_mce { print }
243
+ ' "$TICKET")
244
+ if echo "$MCE_BLOCK" | grep -qE 'Step 6 \[[ -]\]'; then
245
+ flag "P14 drift (NIT): MCE Action 1 row contains 'Step 6 [ ]' / 'Step 6 [-]' after merge — flip to [x] and append squash + housekeeping SHAs"
246
+ fi
247
+ fi
248
+ ```
249
+
250
+ **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`.
251
+ ```bash
252
+ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
253
+ AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
254
+ | grep -iE 'post-deploy|post-merge|production parity|prod verification|on dev API|on prod')
255
+ while IFS= read -r line; do
256
+ [ -z "$line" ] && continue
257
+ echo "$line" | grep -q '\[x\]' || continue
258
+ ac_id=$(echo "$line" | grep -oE 'AC-[A-Za-z0-9_-]+' | head -1)
259
+ [ -z "$ac_id" ] && continue
260
+ if ! echo "$COMPLETION" | grep -E "^\|[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}" | grep -qF "$ac_id"; then
261
+ flag "P15 drift: $ac_id marked [x] with post-deploy semantics but no dated Completion Log entry anchoring this AC-ID"
262
+ fi
263
+ done <<< "$AC_LINES"
264
+ ```
265
+
266
+ **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.
267
+ ```bash
268
+ TRACKER=docs/project_notes/product-tracker.md
269
+ case "$TICKET_STATUS" in
270
+ "Ready for Merge"|"Done")
271
+ if [ -f "$TRACKER" ]; then
272
+ if ! grep -qE "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" "$TRACKER"; then
273
+ flag "P16 drift (NIT): $FEATURE_ID not in any Features table row (must appear as first cell in '| FEATURE_ID | ... |' form)"
274
+ fi
275
+ fi
276
+ ;;
277
+ esac
278
+ ```
279
+
172
280
  ### Execution discipline (added v0.18.1)
173
281
 
174
- For each of the 11 drift checks (P1–P11), 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.
282
+ 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.
175
283
 
176
284
  Recommended pattern:
177
285
 
@@ -208,7 +316,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
208
316
 
209
317
  **STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
210
318
 
211
- ### Drift (12-22) — advisory, refresh before user authorization
319
+ ### Drift (12-27) — advisory, refresh before user authorization
212
320
 
213
321
  | # | Pattern | Status | Detail |
214
322
  |---|---------|:------:|--------|
@@ -223,6 +331,11 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
223
331
  | 20 | P9 Tracker header stale | PASS | header = detail |
224
332
  | 21 | P10 Duplicate log rows | PASS | no duplicates |
225
333
  | 22 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
334
+ | 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
335
+ | 24 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
336
+ | 25 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
337
+ | 26 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
338
+ | 27 | P16 Feature missing from tracker | PASS | feature in Features table |
226
339
 
227
340
  **DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
228
341
 
@@ -242,18 +355,23 @@ Fix them directly:
242
355
  - Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
243
356
  - Data file issues → fix the data
244
357
 
245
- **Drift advisories (12-22) fixes:**
358
+ **Drift advisories (12-27) fixes:**
246
359
  - **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
247
360
  - **P2 (Aspirational Evidence)** → rewrite `[x]` rows with past-tense text + commit SHA + concrete numbers
248
361
  - **P3 (Post-merge action unlogged)** → add a Completion Log row documenting the post-merge execution with date + action + empirical result
249
362
  - **P4 (Remote branch orphan)** → `git push origin --delete <branch>` after confirming merge succeeded
250
363
  - **P5 (Frozen ticket Status)** → update each ticket's `**Status:**` field from "In Progress"/"Ready for Merge" to `Done`; this often belongs in a docs-only tracker-sync PR if the cycle is retroactive
251
- - **P6 (AC count off-by-N)** → recount AC items; update the Merge Checklist Evidence row 1 claim to match actual
364
+ - **P6 (AC count off-by-N)** → recount AC items; update the Merge Checklist Evidence row 1 claim. Use canonical `AC: <marked>/<total>` form (see `merge-checklist.md`) — `total` includes intentionally-deferred ACs
252
365
  - **P7 (Intra-ticket test drift)** → refresh AC / DoD / tracker numbers to match the Completion Log terminal entry
253
366
  - **P8 (Completion Log gap)** → add a Completion Log row per missing Step with agent verdict + commit SHA
254
367
  - **P9 (Tracker header stale)** → update `**Last Updated:**` line step reference to match Active Feature detail
255
368
  - **P10 (Duplicate log rows)** → remove duplicate rows
256
369
  - **P11 (Tracker status mismatch)** → sync tracker Features row status to ticket header Status
370
+ - **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
371
+ - **P13 (key_facts delta mismatch)** → reconcile: either correct the ticket's claimed delta (`+N atoms`/`+M aliases`) to match the actual key_facts.md row, or update key_facts.md to match the truthful delta
372
+ - **P14 (MCE Action 1 stale post-merge)** → flip Step 6 checkbox to `[x]` and remove the `(this merge)` qualifier; append squash SHA + housekeeping SHA to the Workflow evidence line
373
+ - **P15 (Post-deploy AC without evidence)** → add a dated Completion Log row anchoring the AC-ID with empirical results from the production verification (QA battery output, dev-API smoke result, telemetry confirmation); OR re-mark the AC `[ ]` until verification lands
374
+ - **P16 (Feature missing from tracker)** → add a row to the relevant `## Features — *` table in `product-tracker.md` (or document explicitly in the ticket why standalone is intentional with a tracker-side note)
257
375
 
258
376
  After fixing, re-run the audit to confirm all checks pass.
259
377
 
@@ -72,6 +72,8 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
72
72
  | 0. Validate ticket structure | [x] | Sections verified: Spec, Plan, AC, DoD, Workflow, Log, Evidence |
73
73
  | 1. Mark all items | [x] | AC: 12/12, DoD: 7/7, Workflow: 0-5/6 |
74
74
 
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
+
75
77
  ## Action 9: Run compliance audit
76
78
 
77
79
  Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.
@@ -52,12 +52,21 @@ Run only if `git diff origin/<target-branch>..HEAD --name-only` shows `.json` fi
52
52
 
53
53
  Eleven 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
54
 
55
- **12. P1 — PR body test count stale.** Ratio must co-occur with test/pass/green marker to avoid AC/DoD ratios (14/14, 7/7).
55
+ **12. 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
56
  ```bash
57
- PR_BODY=$(gh pr view --json body -q .body)
58
- PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
59
- TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
60
- [ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
57
+ TEST_KW_RE='(npm test|pnpm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])'
58
+ PR_BODY=$(gh pr view --json body -q .body 2>/dev/null || true)
59
+ PR_RATIOS=$(echo "$PR_BODY" | grep -iE "$TEST_KW_RE" | grep -oE "[0-9]+/[0-9]+" | sort -u)
60
+ TICKET_RATIOS=$(grep -iE "$TEST_KW_RE" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | sort -u)
61
+ if [ -z "$PR_RATIOS" ] || [ -z "$TICKET_RATIOS" ]; then
62
+ echo "P1 N/A: no comparable test-count ratios extracted (PR=$(echo "$PR_RATIOS" | tr '\n' ',' ), ticket=$(echo "$TICKET_RATIOS" | tr '\n' ',' ))"
63
+ else
64
+ while IFS= read -r r; do
65
+ [ -z "$r" ] && continue
66
+ echo "$TICKET_RATIOS" | grep -qFx "$r" \
67
+ || flag "P1 drift: PR ratio $r not found in ticket evidence (ticket ratios: $(echo "$TICKET_RATIOS" | tr '\n' ' '))"
68
+ done <<< "$PR_RATIOS"
69
+ fi
61
70
  ```
62
71
 
63
72
  **13. P2 — Merge Checklist Evidence aspirational.** `[x]` rows with future-tense text.
@@ -94,6 +103,8 @@ for t in docs/tickets/*.md; do
94
103
  status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
95
104
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
96
105
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
106
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
107
+ | sed -E 's/\*\*[[:space:]]*$//' \
97
108
  | sed -E 's/[[:space:]]+$//')
98
109
  [ "$status" = "Done" ] && continue
99
110
  ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
@@ -103,12 +114,27 @@ done
103
114
  [ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
104
115
  ```
105
116
 
106
- **17. P6 — AC count off-by-N.**
117
+ **17. P6 — AC count off-by-N.** Two claim forms supported: `all N marked` (N = total) and `AC: X/Y done` (X = marked, Y = total — handles deferred ACs where Y > X).
107
118
  ```bash
108
- ACTUAL=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET" | grep -cE "^- \[[x ]\]")
109
- CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | grep -oE "[0-9]+" | head -1)
110
- [ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL" ] && [ $((ACTUAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL)) -ge 2 ] \
111
- && flag "P6 drift: claim '$CLAIMED' vs actual AC count $ACTUAL"
119
+ 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\]")
122
+ CLAIM_LINE=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1)
123
+ if echo "$CLAIM_LINE" | grep -qE '^AC: [0-9]+/[0-9]+'; then
124
+ CLAIMED_MARKED=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | head -1)
125
+ CLAIMED_TOTAL=$(echo "$CLAIM_LINE" | grep -oE '[0-9]+' | tail -1)
126
+ [ -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"
129
+ [ -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"
132
+ elif [ -n "$CLAIM_LINE" ]; then
133
+ 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"
137
+ fi
112
138
  ```
113
139
 
114
140
  **18. P7 — Test count drift within ticket (final-sections only).**
@@ -157,6 +183,8 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
157
183
  TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
158
184
  | sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
159
185
  | sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
186
+ | sed -E 's/[[:space:]]+(\(.*\)|—.*|–.*|-.*)$//' \
187
+ | sed -E 's/\*\*[[:space:]]*$//' \
160
188
  | sed -E 's/[[:space:]]+$//')
161
189
  FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
162
190
  TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
@@ -169,9 +197,89 @@ esac
169
197
  && flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
170
198
  ```
171
199
 
200
+ **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.
201
+ ```bash
202
+ TRACKER=docs/project_notes/product-tracker.md
203
+ if [ -f "$TRACKER" ]; then
204
+ ACTUAL_HEAD=$(git rev-parse HEAD 2>/dev/null || true)
205
+ if [ -n "$ACTUAL_HEAD" ]; then
206
+ ACTUAL_SHORT=$(printf '%s' "$ACTUAL_HEAD" | cut -c1-7)
207
+ TRACKER_HEADS=$(grep -E '^\*\*(Last Updated|Active Feature):\*\*' "$TRACKER" 2>/dev/null \
208
+ | grep -oE 'HEAD[[:space:]:]+`?[a-f0-9]{7,40}`?' \
209
+ | grep -oE '[a-f0-9]{7,40}' \
210
+ | sort -u || true)
211
+ for sha in $TRACKER_HEADS; do
212
+ case "$ACTUAL_HEAD" in "$sha"*) continue ;; esac
213
+ case "$sha" in "$ACTUAL_SHORT"*) continue ;; esac
214
+ flag "P12 drift: tracker HEAD reference $sha does not match git rev-parse HEAD ($ACTUAL_HEAD); refresh tracker"
215
+ done
216
+ fi
217
+ fi
218
+ ```
219
+
220
+ **24. P13 — key_facts delta vs ticket atom-count mismatch (added v0.18.3).** Whitespace-safe iteration + FEATURE_ID anchoring.
221
+ ```bash
222
+ KEY_FACTS=docs/project_notes/key_facts.md
223
+ if [ -f "$KEY_FACTS" ]; then
224
+ FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
225
+ TICKET_DELTAS=$(grep -oE '\+[0-9]+ (atoms?|aliases?|dishes?|entries|rows)' "$TICKET" | sort -u)
226
+ KEY_FACTS_BLOCK=$(grep -A 3 -F "$FEATURE_ID" "$KEY_FACTS" 2>/dev/null || true)
227
+ while IFS= read -r claim; do
228
+ [ -z "$claim" ] && continue
229
+ if [ -z "$KEY_FACTS_BLOCK" ] || ! echo "$KEY_FACTS_BLOCK" | grep -qF "$claim"; then
230
+ flag "P13 drift: ticket claims '$claim' but key_facts.md block for $FEATURE_ID lacks the same delta"
231
+ fi
232
+ done <<< "$TICKET_DELTAS"
233
+ fi
234
+ ```
235
+
236
+ **25. 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
+ ```bash
238
+ if [ "$TICKET_STATUS" = "Done" ]; then
239
+ MCE_BLOCK=$(awk '
240
+ /^## Merge Checklist Evidence/ { in_mce=1; print; next }
241
+ in_mce && /^## / { in_mce=0 }
242
+ in_mce { print }
243
+ ' "$TICKET")
244
+ if echo "$MCE_BLOCK" | grep -qE 'Step 6 \[[ -]\]'; then
245
+ flag "P14 drift (NIT): MCE Action 1 row contains 'Step 6 [ ]' / 'Step 6 [-]' after merge — flip to [x] and append squash + housekeeping SHAs"
246
+ fi
247
+ fi
248
+ ```
249
+
250
+ **26. P15 — AC with post-deploy keyword admitted without Completion Log evidence (added v0.18.3).** Line-safe iteration via `while IFS= read -r`.
251
+ ```bash
252
+ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
253
+ AC_LINES=$(grep -nE '^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]+AC-[A-Za-z0-9_-]+' "$TICKET" \
254
+ | grep -iE 'post-deploy|post-merge|production parity|prod verification|on dev API|on prod')
255
+ while IFS= read -r line; do
256
+ [ -z "$line" ] && continue
257
+ echo "$line" | grep -q '\[x\]' || continue
258
+ ac_id=$(echo "$line" | grep -oE 'AC-[A-Za-z0-9_-]+' | head -1)
259
+ [ -z "$ac_id" ] && continue
260
+ if ! echo "$COMPLETION" | grep -E "^\|[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}" | grep -qF "$ac_id"; then
261
+ flag "P15 drift: $ac_id marked [x] with post-deploy semantics but no dated Completion Log entry anchoring this AC-ID"
262
+ fi
263
+ done <<< "$AC_LINES"
264
+ ```
265
+
266
+ **27. 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
+ ```bash
268
+ TRACKER=docs/project_notes/product-tracker.md
269
+ case "$TICKET_STATUS" in
270
+ "Ready for Merge"|"Done")
271
+ if [ -f "$TRACKER" ]; then
272
+ if ! grep -qE "^\|[[:space:]]*$FEATURE_ID[[:space:]]*\|" "$TRACKER"; then
273
+ flag "P16 drift (NIT): $FEATURE_ID not in any Features table row (must appear as first cell in '| FEATURE_ID | ... |' form)"
274
+ fi
275
+ fi
276
+ ;;
277
+ esac
278
+ ```
279
+
172
280
  ### Execution discipline (added v0.18.1)
173
281
 
174
- For each of the 11 drift checks (P1–P11), 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.
282
+ 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.
175
283
 
176
284
  Recommended pattern:
177
285
 
@@ -208,7 +316,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
208
316
 
209
317
  **STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
210
318
 
211
- ### Drift (12-22) — advisory, refresh before user authorization
319
+ ### Drift (12-27) — advisory, refresh before user authorization
212
320
 
213
321
  | # | Pattern | Status | Detail |
214
322
  |---|---------|:------:|--------|
@@ -223,6 +331,11 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
223
331
  | 20 | P9 Tracker header stale | PASS | header = detail |
224
332
  | 21 | P10 Duplicate log rows | PASS | no duplicates |
225
333
  | 22 | P11 Tracker status mismatch | PASS | status consistent |
334
+ | 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
335
+ | 24 | P13 key_facts delta mismatch | PASS | N/A — no quantified deltas |
336
+ | 25 | P14 MCE Action 1 stale post-merge | PASS | N/A pre-merge / row past-tense |
337
+ | 26 | P15 Post-deploy AC without evidence | PASS | no post-deploy keyword in ACs |
338
+ | 27 | P16 Feature missing from tracker | PASS | feature in Features table |
226
339
 
227
340
  **DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
228
341
 
@@ -242,18 +355,23 @@ Fix them directly:
242
355
  - Merge base diverged → `git merge origin/<target-branch>` and resolve conflicts
243
356
  - Data file issues → fix the data
244
357
 
245
- **Drift advisories (12-22) fixes:**
358
+ **Drift advisories (12-27) fixes:**
246
359
  - **P1** → edit PR body npm test line to match ticket terminal count
247
360
  - **P2** → rewrite `[x]` rows with past-tense + commit SHA
248
361
  - **P3** → add Completion Log row for each post-merge execution
249
362
  - **P4** → `git push origin --delete <branch>` after merge
250
363
  - **P5** → update ticket Status from "In Progress"/"Ready for Merge" to `Done`
251
- - **P6** → recount ACs and update Merge Checklist row 1 claim
364
+ - **P6** → recount ACs; use canonical `AC: <marked>/<total>` form (see `merge-checklist.md`) — `total` includes intentionally-deferred ACs
252
365
  - **P7** → sync AC/DoD/tracker numbers to Completion Log terminal
253
366
  - **P8** → add Completion Log row per missing Step with agent verdict + commit SHA
254
367
  - **P9** → refresh `**Last Updated:**` step reference
255
368
  - **P10** → remove duplicate rows
256
369
  - **P11** → sync tracker Features row status to ticket header Status
370
+ - **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
371
+ - **P13 (key_facts delta mismatch)** → reconcile ticket delta claim with key_facts.md feature row
372
+ - **P14 (MCE Action 1 stale post-merge)** → flip Step 6 checkbox to `[x]` and remove the `(this merge)` qualifier
373
+ - **P15 (Post-deploy AC without evidence)** → add a dated Completion Log row with empirical post-deploy results, OR re-mark the AC `[ ]` until verification lands
374
+ - **P16 (Feature missing from tracker)** → add a row to the relevant `## Features — *` table in `product-tracker.md`
257
375
 
258
376
  After fixing, re-run the audit to confirm all checks pass.
259
377
 
@@ -72,6 +72,8 @@ In the ticket, fill the `## Merge Checklist Evidence` table. For each action (0
72
72
  | 0. Validate ticket structure | [x] | Sections verified: Spec, Plan, AC, DoD, Workflow, Log, Evidence |
73
73
  | 1. Mark all items | [x] | AC: 12/12, DoD: 7/7, Workflow: 0-5/6 |
74
74
 
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
+
75
77
  ## Action 9: Run compliance audit
76
78
 
77
79
  Run `/audit-merge` to verify all compliance checks pass automatically. If any check fails, fix it and re-run until all pass.