devflow-kit 2.2.0 → 2.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to Devflow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.3.0] - 2026-08-31
9
+
10
+ ### Added
11
+ - **`refresh-anchor` ledger op**: post-promotion reinforcement now reaches rendered output. When the Learning agent reinforces an already-anchored observation (sharpening its `pattern`/`details`), calling `refresh-anchor <anchor_id>` re-projects the updated log row through the same `toLedgerRow` projector as `assign-anchor` and re-renders all three `.md` files. Previously, post-promotion sharpening was written to the log but never projected forward, so the rendered entry silently froze at its first-promotion snapshot.
12
+
13
+ ### Changed
14
+ - **`decisions-ledger.jsonl` is now the anchor registry only (ADR-022)**: `decisions-log.jsonl` is the content authority; the ledger holds anchor numbers and `decisions_status` only. Entry content reaches the ledger exclusively through `assign-anchor` (first promotion) and `refresh-anchor` (post-promotion re-projection) via `toLedgerRow`. The Learning agent's previously sanctioned path of editing ledger rows directly is removed — content changes go to the log, then `refresh-anchor` re-projects. Tooling that reads or writes `decisions-ledger.jsonl` directly is affected.
15
+ - **`/resolve` DUPLICATE verdict**: `/resolve` now collapses duplicate cross-reviewer findings via a new `DUPLICATE` triage verdict — resolution-summary counts unique issues, with a `Duplicates Collapsed` statistics row and a `## Duplicates` section for traceability.
16
+
17
+ ### Fixed
18
+ - **Semicolon-safe `details` field parsing**: the decisions formatter now splits `details` into fields using a segment-aware parser (`segmentDetails`) instead of delimiter regexes. The parser recognises a segment as a new field only when it starts with a known key name followed by `:` (anchored to segment start); semicolons inside values are preserved. Fixes four related defects: truncation at the first internal semicolon, unanchored-key false match (e.g. `reissue:` matching `issue:`), first-match-wins hijack (a key name mentioned inside an earlier value would capture the wrong segment), and newline breakage. Measured blast radius on this repo's own ledger: 111 truncated field extractions before the fix, 0 after. **Installed projects' rendered decisions/pitfalls `.md` may show one-time `--check` drift under the new parser — self-heals on the next ledger op.**
19
+ - **Armed double-assign guard**: `assign-anchor` now writes `anchor_id` back to the log row on promotion, enabling the guard that prevents re-anchoring an already-anchored observation. Previously `anchor_id` was never written back, so the guard was permanently inert and running `assign-anchor` twice on the same observation silently minted two anchors.
20
+ - **Pitfall date stamping and render date-purity**: `assign-anchor` now stamps a `date` field on all entry types (decisions and pitfalls). Previously only decision rows received a date stamp, leaving the 7-day protection window permanently inert for all pitfall entries. Render formatters now use `row.date || ''` (D5) instead of reading the clock, making renders deterministic and avoiding phantom date changes on re-render.
21
+ - **Amendments rendering**: `formatAmendmentsLine` renders the `amendments` field as a `- **Amendments**: ...` line in the entry body. Previously the projected `amendments` field was never rendered, so amendment notes were lost at the `.md` level. Index extraction regexes are now line-anchored (`/m` flag) to prevent amendment text that mentions `- **Status**:` or `- **Area**:` from hijacking the extracted values.
22
+ - **Working memory worker staged-write CAS** (closes #306): the background memory worker now writes to `WORKING-MEMORY.md.new` (staged file, never the real path) and compare-and-swaps into place only if `WORKING-MEMORY.md` is byte-identical to the pre-run snapshot. Previously the success check accepted any mtime bump on a file whose line 1 carried the stamp prefix — including a human's own concurrent edit — and on that false success the worker deleted the unprocessed queue batch and touched `.last-refresh-ok`, producing silent loss of captured turns under a healthy freshness marker.
23
+ - **Stamped pre-compact bootstrap**: `pre-compact-memory` now writes a HEAD SHA stamp on line 1 of `WORKING-MEMORY.md` (guarded by a 40-hex validation gate) and lays out the five canonical sections in fixed order. Previously the bootstrap ran without a stamp, producing "synced @ unknown" at the next SessionStart and an incorrect State-A classification that hid any real drift.
24
+ - **Reconciliation-aware worker prompt**: the memory worker prompt now includes bounded git evidence since the last stamp, explicit reconciliation and expiry guidance, and a strict DONE definition (per PF-010). Addresses unbounded carry-forward, conversation-coined labels promoted to durable state, and no-expiry instruction.
25
+ - **State-C orphaned `.processing` visibility**: `session-start-memory`'s State C queue-depth count now includes lines from any orphaned `.pending-turns.processing` file, not just `.pending-turns.jsonl`. Previously an orphaned `.processing` was invisible to the State C detector, so a CONFLICT-requeued batch did not show in the refresh-failing banner.
26
+ - **`/resolve` base branch token**: resolution summaries now render the base branch name instead of a literal `{base}` token. Step 0b was not extracting `base_branch` while the summary template referenced it.
27
+ - **render summary byte counts**: `render-decisions.cjs` now reports file sizes via `Buffer.byteLength()` instead of `String.length`. The em dash separator in index Area fields (U+2014, 3 UTF-8 bytes, 1 JS character) caused the logged index.md size to be 2 bytes per em dash in the rendered index under the old code.
28
+
29
+ ---
30
+
8
31
  ## [2.2.0] - 2026-08-25
9
32
 
10
33
  ### Changed
@@ -1192,6 +1215,7 @@ devflow init
1192
1215
  ---
1193
1216
 
1194
1217
  [Unreleased]: https://github.com/dean0x/devflow/compare/v2.0.0...HEAD
1218
+ [2.3.0]: https://github.com/dean0x/devflow/compare/v2.2.0...v2.3.0
1195
1219
  [2.2.0]: https://github.com/dean0x/devflow/compare/v2.1.0...v2.2.0
1196
1220
  [2.1.0]: https://github.com/dean0x/devflow/compare/v2.0.1...v2.1.0
1197
1221
  [2.0.1]: https://github.com/dean0x/devflow/compare/v2.0.0...v2.0.1
@@ -50,7 +50,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par
50
50
 
51
51
  **If BLOCKED:** In single-worktree mode, stop and report the blocker to user. If no reviews found, suggest `/code-review` or `/bug-analysis` first. In multi-worktree mode, report the failure but continue with other worktrees.
52
52
 
53
- **Extract from response:** `branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
53
+ **Extract from response:** `branch`, `base_branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
54
54
 
55
55
  **Fetch PR body** (after extracting `pr_number`):
56
56
  ```bash
@@ -186,7 +186,7 @@ Issues are extracted from `{TARGET_DIR}` only — never cross-reference reviews
186
186
  ### Phase 1b: Fetch External Review Threads (Compliance-gated)
187
187
 
188
188
  **Produces:** THREAD_MAP
189
- **Requires:** PR_INFO, COMPLIANCE_SKILL_INSTALLED
189
+ **Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
190
190
 
191
191
  Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
192
192
 
@@ -205,7 +205,7 @@ Parse `THREAD_MAP` from Git agent output. If Git agent returns `TRACEABILITY: DE
205
205
  ### Phase 2: Global Triage
206
206
 
207
207
  **Produces:** TRIAGE_RESULTS
208
- **Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION, BRANCH_INFO
208
+ **Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION
209
209
 
210
210
  Spawn a single global Triage agent for ALL issues:
211
211
 
@@ -217,7 +217,7 @@ WORKTREE_PATH: {worktree_path} (omit if cwd)
217
217
  DECISIONS_CONTEXT: {decisions_context}
218
218
  FEATURE_KNOWLEDGE: {feature_knowledge}
219
219
  PR_DESCRIPTION: <pr-description>{pr_description}</pr-description>
220
- Triage every issue using the blast-radius disposition matrix. Assign exactly one verdict per issue.
220
+ Triage every issue: collapse duplicates first, then apply the blast-radius disposition matrix to each group's primary. Assign exactly one verdict per issue.
221
221
  Follow devflow:apply-decisions to Read full ADR/PF bodies on demand.
222
222
  Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE."
223
223
  ```
@@ -229,11 +229,12 @@ Wait for Triage agent to complete before proceeding. Parse verdict ledger from T
229
229
  - **BY_DESIGN**: Intentional code (with ADR or code doc citation)
230
230
  - **FIX_SEPARATE**: Valid but out of blast-radius scope (must become manage-debt ticket)
231
231
  - **TECH_DEBT**: Architectural overhaul only — LAST RESORT
232
+ - **DUPLICATE**: Collapsed duplicate issue — carries `duplicate_of: <primary-id>` referencing the non-DUPLICATE primary; inherits the primary's outcome
232
233
 
233
234
  Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning columns.
234
235
 
235
236
  **Triage agent completeness assertion (avoids PF-002):** Verify the parsed ledger against ISSUES before proceeding:
236
- 1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets.
237
+ 1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets. DUPLICATE is a valid bucket; a valid DUPLICATE entry must name its `duplicate_of` primary (the `Duplicate Of` column of the ledger's DUPLICATE table) and that primary must be a non-DUPLICATE issue id. A missing `duplicate_of` or one that chains to another DUPLICATE is a **Triage agent failure** (retry-then-abort as below).
237
238
  2. If the Triage agent output is empty, contains a skill re-entrancy guard string (e.g., contains `already running`), or is missing any issue IDs from ISSUES: treat as a **Triage agent failure**:
238
239
  - Retry the Triage agent once with the same inputs.
239
240
  - If the retry also fails the completeness check: abort with a clear error message listing the missing issue IDs and failure reason — never proceed with dropped issues.
@@ -247,7 +248,7 @@ Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning c
247
248
 
248
249
  If FIX_NOW list is empty: skip to Phase 5 — write full summary (Phase 5), run manage-debt (Phase 9) if FIX_SEPARATE/TECH_DEBT exist, run thread resolution + resolution comment (Phase 9b), run merge readiness (Phase 9c), display results (Phase 10).
249
250
 
250
- Otherwise, batch FIX_NOW issues for Code agent execution:
251
+ Otherwise, batch FIX_NOW issues for Code agent execution. **DUPLICATE issues are never dispatched** — they inherit the primary's outcome:
251
252
  - **Same-file issues** → one batch (one Code agent per file, sequential for same-file pairs)
252
253
  - **Distinct-file issues** → parallel Code agents
253
254
  - **Max 5 issues per batch** — chunk large sets
@@ -255,7 +256,7 @@ Otherwise, batch FIX_NOW issues for Code agent execution:
255
256
  ### Phase 4: Fix (Code agent × N)
256
257
 
257
258
  **Produces:** CODE_AGENT_RESULTS
258
- **Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, BRANCH_INFO
259
+ **Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE
259
260
 
260
261
  For each batch, spawn Code agent with `OPERATION: issue-fix` and `PUSH: false`:
261
262
 
@@ -284,12 +285,14 @@ Collect from each Code agent:
284
285
  ### Phase 5: Write resolution-summary.md
285
286
 
286
287
  **Produces:** RESOLUTION_FILE (early write for compaction safety)
287
- **Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS
288
+ **Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS, TARGET_DIR, BRANCH_INFO
288
289
 
289
290
  **Immediately write `resolution-summary.md`** to `{TARGET_DIR}` using the Write tool. Do this now — not in Phase 9 — while results are fresh in context. This ensures the record is persisted even if later phases (Simplify, Verification Gate, CI gate, Tech Debt) trigger context compaction.
290
291
 
291
292
  Set `Tracked` for FIX_SEPARATE and TECH_DEBT items to `(pending)` — to be backfilled after Phase 9 manage-debt.
292
293
 
294
+ DUPLICATE issues are listed **only** in `## Duplicates` — never in `## Fixed Issues`, `## False Positives`, `## By Design`, `## Fix Separately`, `## Deferred to Tech Debt`, `## Escalations`, or `## Blocked`. A duplicate of a FALSE_POSITIVE primary therefore leaves only the primary in the `False Positive` row and the `## False Positives` section; the same holds for every other outcome the duplicate inherits.
295
+
293
296
  Use the template from the Output Artifact section below.
294
297
 
295
298
  ### Phase 6: Simplify
@@ -382,7 +385,7 @@ Otherwise, for each worktree with fixes:
382
385
 
383
386
  **IMPORTANT**: Run sequentially across all worktrees (not in parallel) to avoid GitHub API conflicts.
384
387
 
385
- If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent:
388
+ If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent. **DUPLICATE issues never create their own debt tickets** — a duplicate of a deferred primary is covered by the primary's ticket:
386
389
 
387
390
  ```
388
391
  Agent(subagent_type="Git"):
@@ -407,6 +410,7 @@ Skip this step if `COMPLIANCE_SKILL_INSTALLED` is false or THREAD_MAP is empty.
407
410
 
408
411
  Prepare THREAD_MAP with verdicts from triage/code agent results:
409
412
  - For each `ext-{N}`: match to an issue verdict (FIXED, FALSE_POSITIVE, BY_DESIGN, ESCALATED) by `file:line` correlation
413
+ - If the matched issue has verdict DUPLICATE, use the **primary's** verdict and verification status for the reply — do not expose DUPLICATE to the thread author (avoids PF-024; caller-side mapping — git.md contracts unchanged)
410
414
  - Include `commit_sha` from Code agent results for FIXED verdicts
411
415
  - Unmatched threads: ESCALATED (human review)
412
416
 
@@ -447,7 +451,7 @@ Update `## Third-Party Threads` section in resolution-summary.md with thread res
447
451
  ### Phase 9c: Merge Readiness (Compliance-gated, Report-only)
448
452
 
449
453
  **Produces:** MERGE_READINESS_REPORT
450
- **Requires:** PR_INFO, COMPLIANCE_SKILL_INSTALLED
454
+ **Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
451
455
 
452
456
  Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
453
457
 
@@ -466,7 +470,7 @@ If Git agent returns `TRACEABILITY: DEGRADED`: warn, proceed to Phase 10.
466
470
 
467
471
  ### Phase 10: Report
468
472
 
469
- **Requires:** TARGET_DIR
473
+ **Requires:** BRANCH_INFO, TARGET_DIR
470
474
 
471
475
  The resolution summary was already written to `{TARGET_DIR}/resolution-summary.md` in Phase 5 (updated by Phase 7 and Phase 9). Display results to the user:
472
476
 
@@ -486,6 +490,7 @@ The resolution summary was already written to `{TARGET_DIR}/resolution-summary.m
486
490
  | Deferred | {n} |
487
491
  | Blocked | {n} |
488
492
  | Escalated | {n} |
493
+ | Duplicates Collapsed | {n} |
489
494
 
490
495
  ### Verification
491
496
  Final gate: {PASS | FAILED after N attempts}
@@ -578,9 +583,9 @@ After writing, commit the two files to the current worktree branch yourself by r
578
583
  │ └─ Git agent (fetch-review-threads) → THREAD_MAP
579
584
  │
580
585
  ├─ Phase 2: Global Triage [Triage agent, opus, single agent]
581
- │ └─ ALL issues → verdict ledger by disposition
586
+ │ └─ ALL issues → verdict ledger by disposition (incl. DUPLICATE with duplicate_of)
582
587
  │
583
- ├─ Phase 3: Batch FIX_NOW issues (skip if empty)
588
+ ├─ Phase 3: Batch FIX_NOW issues (skip if empty; DUPLICATE issues never dispatched)
584
589
  │ └─ same-file sequential, distinct-file parallel, max 5/batch
585
590
  │
586
591
  ├─ Phase 4: Fix [Code agent × N, OPERATION: issue-fix, PUSH: false]
@@ -623,6 +628,8 @@ After writing, commit the two files to the current worktree branch yourself by r
623
628
  | Worktree pre-flight fails | Report failure, continue with other worktrees |
624
629
  | Empty FIX_NOW list | Skip Phases 3-4/6-8; still write full summary + run manage-debt if FIX_SEPARATE/TECH_DEBT exist |
625
630
  | ESCALATED security issues | Surfaced in ## Escalations + display callout; never routed to manage-debt |
631
+ | DUPLICATE verdict without duplicate_of, or chained to another DUPLICATE | Treated as Triage failure — same retry-then-abort as a vanished id |
632
+ | DUPLICATE issues in THREAD_MAP | Map ext-{N} to primary's verdict/verification status for thread reply |
626
633
  | Verification Gate FAILED after 2 attempts | Recorded as FAILED in ## Verification + blocking callout; CI gate skipped; proceed to Phase 9 (manage-debt) then Phase 10 (display) |
627
634
  | gh/GitHub absent | manage-debt fails gracefully; Tracked stays "(pending)" + noted — recorded, not dropped |
628
635
  | COMPLIANCE_SKILL_INSTALLED false | Phases 1b, 9b-step-1, and 9c are skipped; post-resolution-summary (Phase 9b step 2) still runs if a PR is known |
@@ -651,7 +658,7 @@ Written in Phase 5 (Collect Results) to `{TARGET_DIR}/resolution-summary.md`:
651
658
  ```markdown
652
659
  # Resolution Summary
653
660
 
654
- **Branch**: {branch} -> {base}
661
+ **Branch**: {branch} -> {base_branch}
655
662
  **Date**: {timestamp}
656
663
  **Review**: {TARGET_DIR}
657
664
  **Command**: /resolve
@@ -673,8 +680,9 @@ Written in Phase 5 (Collect Results) to `{TARGET_DIR}/resolution-summary.md`:
673
680
  | Deferred | {n} |
674
681
  | Blocked | {n} |
675
682
  | Escalated | {n} |
683
+ | Duplicates Collapsed | {n} |
676
684
 
677
- _(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser.)_
685
+ _(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser. `Total Issues` counts every triaged issue including collapsed duplicates; every row **between** `Total Issues` and `Duplicates Collapsed` counts UNIQUE (non-DUPLICATE) issues only, so `Total Issues` equals the sum of the rows below it. Excluding duplicates from `Fixed`, `False Positive`, and `Deferred` de-skews the fp\_ratio convergence formula in code-review without any parser change.)_
678
686
 
679
687
  ## Verification
680
688
  | Command | Result |
@@ -720,6 +728,11 @@ Final gate: PASS | FAILED after {n} attempts
720
728
  |-------|-----------|---------|
721
729
  | {description} | {file}:{line} | {why} |
722
730
 
731
+ ## Duplicates
732
+ | Issue | Duplicate Of | File:Line |
733
+ |-------|-------------|-----------|
734
+ | {description} | {primary-id} | {file}:{line} |
735
+
723
736
  ## Third-Party Threads
724
737
  | Thread | File:Line | Verdict | Status |
725
738
  |--------|-----------|---------|--------|
@@ -728,4 +741,4 @@ Final gate: PASS | FAILED after {n} attempts
728
741
 
729
742
  _(Omit `## Third-Party Threads` if `COMPLIANCE_SKILL_INSTALLED` is false or no external threads were found.)_
730
743
 
731
- **Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `/code-review` convergence parser reads the `Deferred`, `Fixed`, and `False Positive` Statistics rows plus the `## Fixed Issues` / `## False Positives` headings — keep those labels byte-stable.
744
+ **Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `Duplicates Collapsed` row is additive — the `/code-review` convergence parser reads only `Deferred`, `Fixed`, and `False Positive` rows plus `## Fixed Issues` / `## False Positives` headings — keep those labels byte-stable. All rows that the parser reads count UNIQUE (non-DUPLICATE) issues only, so collapsed duplicates do not inflate fp\_ratio.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-kit",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A meta-harness for Claude Code — turns a single coding agent into an engineering team: orchestration, parallel review, persistent memory, self-learning, and graph workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@ skills:
18
18
  You process the pending decisions queue for one project: claim it atomically, detect
19
19
  decision/pitfall patterns worth keeping, curate the existing ledger, and delete the claimed
20
20
  queue as your final act. You read and edit the data files directly — no script reads,
21
- validates, or applies anything on your behalf. The only executables you call are the three
21
+ validates, or applies anything on your behalf. The only executables you call are the four
22
22
  ledger ops below.
23
23
 
24
24
  ## Iron Law
@@ -26,11 +26,12 @@ ledger ops below.
26
26
  > **assign-anchor OWNS NUMBERING; render OWNS THE .md; NEVER HAND-EDIT decisions.md, pitfalls.md, or index.md**
27
27
  >
28
28
  > ADR and PF numbers are assigned exclusively by `assign-anchor`. The `.md` files are written
29
- > exclusively by `render-decisions.cjs` (invoked internally by `assign-anchor`/`retire-anchor`).
30
- > One `assign-anchor` invocation claims one number and re-renders all three files atomically
31
- > (decisions.md, pitfalls.md, index.md). To deprecate, supersede, or retire an entry, call
32
- > `retire-anchor <anchor_id> <status>` — never edit the `.md` files directly. Manual re-render
33
- > via `render-decisions.cjs render "$(pwd)"` also refreshes index.md.
29
+ > exclusively by `render-decisions.cjs` (invoked internally by `assign-anchor`/`retire-anchor`/`refresh-anchor`).
30
+ > One `assign-anchor` invocation claims one number and re-renders all three files
31
+ > (decisions.md, pitfalls.md, index.md — each write atomic; the sequence is not transactional:
32
+ > a crash between writes self-heals on the next op). To deprecate, supersede, or retire an entry, call
33
+ > `retire-anchor <anchor_id> <status>` — never edit the `.md` files directly. Every ledger op
34
+ > re-renders all three files internally; there is no separate render step for you to run.
34
35
 
35
36
  ## Environment
36
37
 
@@ -39,6 +40,7 @@ are relative to it. The ledger ops live at `$HOME/.devflow/scripts/hooks/json-he
39
40
 
40
41
  - `assign-anchor <type> <obs_id>` — claims the next ADR/PF number and re-renders all three `.md` files (decisions.md, pitfalls.md, index.md)
41
42
  - `retire-anchor <anchor_id> <status>` — flips a ledger row's rendered status and re-renders
43
+ - `refresh-anchor <anchor_id> [<anchor_id>...]` — variadic: re-projects one or more anchored log rows through the same projector as `assign-anchor` in a single lock/parse/render pass; use after reinforcing already-anchored observations (ADR-022)
42
44
  - `rotate-observations` — archives `observing` log rows older than 30 days
43
45
 
44
46
  Each op self-locks internally. Call them plainly — never wrap them in a lock of your own,
@@ -119,6 +121,23 @@ rewrite the whole file:
119
121
  timestamps are UTC ISO (`date -u +%Y-%m-%dT%H:%M:%SZ`). Estimate `confidence` honestly —
120
122
  it is curation metadata only, NOT a gate; do not inflate it.
121
123
 
124
+ **`details` grammar**: use `Key: value` segments separated by `;`. Recognised keys are per
125
+ type and disjoint — decisions: `context:`, `decision:`, `rationale:`; pitfalls: `area:`,
126
+ `issue:`, `impact:`, `resolution:`. A segment that begins with a key recognised FOR THAT TYPE
127
+ starts a new field; any other segment (including a key from the opposite type) is appended to
128
+ the previous field's value, so semicolons inside a value are preserved. Keep prose out of key
129
+ positions — do not start a value with text that looks like a recognised key for that type. The
130
+ parser has a recovery pass for legacy mid-segment keys.
131
+
132
+ **`amendments` field**: when reinforcing an already-anchored observation with a dated
133
+ correction or ratification that should remain visible as history (rather than silently
134
+ rewriting `details`), APPEND `{ "date": "YYYY-MM-DD", "note": "..." }` to the log row's
135
+ `amendments` array (create the array if absent). The shape is exactly `{date, note}` — the
136
+ schema guard rejects bare strings. Amendments render at the end of the entry body in
137
+ `decisions.md`/`pitfalls.md`; they never appear in `index.md` lines. A follow-up
138
+ `refresh-anchor <anchor_id>` is required to propagate the addition to the rendered files
139
+ (ADR-022).
140
+
122
141
  - **Reinforce an existing row** — use the Edit tool to replace that row's single line:
123
142
  increment `observations`, union `evidence` (dedupe, cap 10), update `last_seen`, and
124
143
  refresh `pattern`/`details`/`confidence` only where the new evidence sharpens them.
@@ -134,12 +153,32 @@ node "$HOME/.devflow/scripts/hooks/json-helper.cjs" assign-anchor "pitfall" "obs
134
153
  NEVER hand-edit `decisions.md` or `pitfalls.md`. NEVER invent an ADR-NNN/PF-NNN number
135
154
  yourself — `assign-anchor` is the only source of numbering.
136
155
 
156
+ **After reinforcing already-anchored observations**: once you have updated all target log rows
157
+ (incrementing `observations`, refreshing `pattern`/`details`, updating `last_seen`), collect
158
+ all anchor ids and make ONE variadic call:
159
+
160
+ ```bash
161
+ node "$HOME/.devflow/scripts/hooks/json-helper.cjs" refresh-anchor <anchor_id1> [<anchor_id2> ...]
162
+ ```
163
+
164
+ This re-projects all sharpened log rows in a single lock/parse/render pass, propagating
165
+ improvements to `decisions.md`/`pitfalls.md`/`index.md`. BATCH: do not call once per row — N
166
+ calls pay N full-corpus renders; one variadic call pays one. Refresh calls do not consume
167
+ curation slots; however, at most 10 anchors may be refreshed per run — stop if the cap is
168
+ reached.
169
+
137
170
  ## Part 2 — Curation
138
171
 
139
172
  Periodic housekeeping of the ledger and rendered `.md` files. Bounds: **≤5 curation changes
140
173
  per run**. **7-day protection window** — never touch any entry whose `date` field in the
141
174
  ledger (`.devflow/learning/decisions-ledger.jsonl`) is within the past 7 days. The window key
142
- is the ledger row's `date` field (YYYY-MM-DD), not anything in the `.md` file.
175
+ is the ledger row's `date` field (YYYY-MM-DD), not anything in the `.md` file. If the ledger
176
+ row lacks a `date` field (pitfall rows promoted before date-stamping was added), use the
177
+ observation log row's `last_seen` date for the window. If `last_seen` is also unavailable, the
178
+ entry predates date-stamping and is outside the protection window (no backfill: a fabricated date would be worse than an unprotected entry — ADR-022).
179
+ Example: a pitfall row with no ledger `date` whose log row has `last_seen: "2026-08-27"` → window
180
+ key 2026-08-27 (protected if within 7 days of today); no ledger `date` AND no log `last_seen`
181
+ → outside the window, eligible for curation.
143
182
 
144
183
  Ground yourself first, all by direct reads:
145
184
  - Active entries and counts: `decisions.md` / `pitfalls.md` — what is rendered is what is active.
@@ -148,6 +187,13 @@ Ground yourself first, all by direct reads:
148
187
  those files still exist (Glob). An entry whose referenced files are gone is a preferred
149
188
  retirement candidate — a signal to prefer, not an automatic retirement.
150
189
 
190
+ **PF-040 pointer-vs-citation gate**: before acting on a missing-path signal (a file cited in
191
+ `details`/`evidence` no longer exists), determine whether the reference is a live POINTER (a
192
+ file a reader should follow today) or a HISTORICAL CITATION (the file the entry recorded
193
+ deleting, replacing, or retiring). A missing live pointer is drift — repair the reference. A
194
+ missing historical citation is confirmation that the decision was implemented — leave the entry
195
+ intact.
196
+
151
197
  **Rotate stale observations first** (before selecting curation candidates):
152
198
 
153
199
  ```bash
@@ -180,10 +226,13 @@ node "$HOME/.devflow/scripts/hooks/json-helper.cjs" retire-anchor <anchor_id> <s
180
226
 
181
227
  `retire-anchor` is atomic and idempotent. Call it once per entry.
182
228
 
183
- **Citation preservation**: if an entry being retired has inbound `applies ADR-NNN` citations
184
- in other entries, update those entries' `pattern`/`details` to reference the surviving entry
185
- instead — edit those ledger rows directly (one line at a time), then re-render via
186
- `node "$HOME/.devflow/scripts/hooks/lib/render-decisions.cjs" render "$(pwd)"`.
229
+ **Citation preservation** (ADR-022 — log is content authority): if an entry being retired
230
+ has inbound `applies ADR-NNN` citations in other entries' `pattern`/`details`, update those
231
+ other entries to reference the surviving entry — do this by editing their **log rows** in
232
+ `decisions-log.jsonl` (one line at a time), then collecting all updated anchor ids and calling
233
+ ONCE: `node "$HOME/.devflow/scripts/hooks/json-helper.cjs" refresh-anchor <anchor_id1> [<anchor_id2> ...]`
234
+ Batch all ids into the single variadic call — one lock/parse/render pass for the whole set.
235
+ Never edit the ledger directly for content changes; the log is the authority.
187
236
 
188
237
  **Cap enforcement**: stop after 5 changes regardless of remaining candidates.
189
238
 
@@ -191,8 +240,9 @@ instead — edit those ledger rows directly (one line at a time), then re-render
191
240
 
192
241
  1. Run `rotate-observations` if you have not already this run (Part 2 covers it — never run
193
242
  it twice).
194
- 2. Delete the claim file as your FINAL act, strictly after every other write (bare `rm` is
195
- blocked by devflow's recommended deny-list — PF-003):
243
+ 2. Delete the claim file as your FINAL act, strictly after every other write (`rm -f` is
244
+ denied by devflow's recommended deny-list; `unlink` and a flagless `rm` both pass — use
245
+ `unlink` (PF-003)):
196
246
  `unlink .devflow/learning/.pending-turns.processing`
197
247
  If deletion is denied, finish normally and note the leftover claim file in your summary —
198
248
  the next run's stale-merge recovery folds it in.
@@ -28,10 +28,22 @@ You receive from orchestrator:
28
28
 
29
29
  1. **Read context per issue**: For each issue, Read 30 lines around the reported file:line to understand the actual code.
30
30
  2. **Apply Decisions**: Scan the DECISIONS_CONTEXT index to identify relevant ADR and PF entries. Read full bodies on demand. Cite `applies ADR-NNN` / `avoids PF-NNN` in your Reasoning column. Skip when DECISIONS_CONTEXT is empty or `(none)`. Use only verbatim IDs from the index — do not fabricate.
31
- 3. **Assign disposition**: Apply the blast-radius matrix below. Every issue gets exactly one verdict — none may vanish.
31
+ 3. **Assign disposition**: Run the duplicate grouping pre-pass, then apply the blast-radius matrix to each group's primary. Every issue gets exactly one verdict (DUPLICATE included) — none may vanish.
32
32
  4. **Document evidence**: FALSE_POSITIVE requires cited grep/file:line. BY_DESIGN requires an ADR or inline comment/doc citation.
33
33
  5. **Assign risk tier**: For every FIX_NOW issue, annotate Standard or Careful.
34
34
 
35
+ ## Duplicate Grouping Pre-Pass
36
+
37
+ Run this pre-pass **before** the disposition matrix. It is a relation between issues, not a matrix row.
38
+
39
+ 1. **Group by same defect**: cluster issues that share the same root cause — typically the same or adjacent file:line reported by different review foci, or the same logical error in different phrasings.
40
+ 2. **Select primary**: from each group, designate as primary the most specific and complete report — but when a group mixes security and non-security findings (a 'security member' is one that would trigger the Security Gate), the security member is always the primary. All other members are non-primary duplicates.
41
+ 3. **Security gate applies to the whole group**: if ANY member is a security finding, the group's primary passes through the Security Gate (→ FIX_NOW or ESCALATED only). Never downgrade a group because non-security members outnumber the security finding.
42
+ 4. **Non-primary members**: assign verdict **DUPLICATE** with `duplicate_of: <primary-id>`. Never chain — `duplicate_of` must reference a non-DUPLICATE issue. A DUPLICATE inherits its primary's outcome.
43
+ 5. **Single-member groups**: if an issue has no duplicates it is its own primary — apply the matrix directly.
44
+
45
+ Apply the disposition matrix to each group's **primary only**.
46
+
35
47
  ## Blast-Radius Disposition Matrix
36
48
 
37
49
  **First match wins. Apply in the order listed.**
@@ -117,6 +129,11 @@ Return the verdict ledger grouped by disposition:
117
129
  |----------|-----------|----------------------|
118
130
  | {id} | {file}:{line} | {why requires complete redesign} |
119
131
 
132
+ ### DUPLICATE
133
+ | Issue ID | Duplicate Of | File:Line | Reason |
134
+ |----------|-------------|-----------|--------|
135
+ | {id} | {primary-id} | {file}:{line} | {same defect as {primary-id}, reported by {focus}} |
136
+
120
137
  ### Summary
121
138
  - Total Issues: {n}
122
139
  - ESCALATED: {n}
@@ -125,6 +142,7 @@ Return the verdict ledger grouped by disposition:
125
142
  - BY_DESIGN: {n}
126
143
  - FIX_SEPARATE: {n}
127
144
  - TECH_DEBT: {n}
145
+ - DUPLICATE: {n}
128
146
  ```
129
147
 
130
148
  ## Boundaries
@@ -56,7 +56,7 @@ In multi-worktree mode, spawn all pre-flight agents **in a single message** (par
56
56
 
57
57
  **If BLOCKED:** In single-worktree mode, stop and report the blocker to user. If no reviews found, suggest `/code-review` or `/bug-analysis` first. In multi-worktree mode, report the failure but continue with other worktrees.
58
58
 
59
- **Extract from response:** `branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
59
+ **Extract from response:** `branch`, `base_branch`, `branch_slug`, `pr_number`, `review_count`, `diff_files` per worktree.
60
60
 
61
61
  **Fetch PR body** (after extracting `pr_number`):
62
62
  ```bash
@@ -138,7 +138,7 @@ Issues are extracted from `\{TARGET_DIR\}` only — never cross-reference review
138
138
  ### Phase 1b: Fetch External Review Threads (Compliance-gated)
139
139
 
140
140
  **Produces:** THREAD_MAP
141
- **Requires:** PR_INFO, COMPLIANCE_SKILL_INSTALLED
141
+ **Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
142
142
 
143
143
  Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
144
144
 
@@ -157,7 +157,7 @@ Parse `THREAD_MAP` from Git agent output. If Git agent returns `TRACEABILITY: DE
157
157
  ### Phase 2: Global Triage
158
158
 
159
159
  **Produces:** TRIAGE_RESULTS
160
- **Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION, BRANCH_INFO
160
+ **Requires:** ISSUES, DIFF_FILES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, PR_DESCRIPTION
161
161
 
162
162
  Spawn a single global Triage agent for ALL issues:
163
163
 
@@ -169,7 +169,7 @@ WORKTREE_PATH: {worktree_path} (omit if cwd)
169
169
  DECISIONS_CONTEXT: {decisions_context}
170
170
  FEATURE_KNOWLEDGE: {feature_knowledge}
171
171
  PR_DESCRIPTION: <pr-description>{pr_description}</pr-description>
172
- Triage every issue using the blast-radius disposition matrix. Assign exactly one verdict per issue.
172
+ Triage every issue: collapse duplicates first, then apply the blast-radius disposition matrix to each group's primary. Assign exactly one verdict per issue.
173
173
  Follow devflow:apply-decisions to Read full ADR/PF bodies on demand.
174
174
  Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE."
175
175
  ```
@@ -181,11 +181,12 @@ Wait for Triage agent to complete before proceeding. Parse verdict ledger from T
181
181
  - **BY_DESIGN**: Intentional code (with ADR or code doc citation)
182
182
  - **FIX_SEPARATE**: Valid but out of blast-radius scope (must become manage-debt ticket)
183
183
  - **TECH_DEBT**: Architectural overhaul only — LAST RESORT
184
+ - **DUPLICATE**: Collapsed duplicate issue — carries `duplicate_of: <primary-id>` referencing the non-DUPLICATE primary; inherits the primary's outcome
184
185
 
185
186
  Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning columns.
186
187
 
187
188
  **Triage agent completeness assertion (avoids PF-002):** Verify the parsed ledger against ISSUES before proceeding:
188
- 1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets.
189
+ 1. Every issue `id` from ISSUES must appear in exactly one verdict bucket — none may vanish, none may appear in multiple buckets. DUPLICATE is a valid bucket; a valid DUPLICATE entry must name its `duplicate_of` primary (the `Duplicate Of` column of the ledger's DUPLICATE table) and that primary must be a non-DUPLICATE issue id. A missing `duplicate_of` or one that chains to another DUPLICATE is a **Triage agent failure** (retry-then-abort as below).
189
190
  2. If the Triage agent output is empty, contains a skill re-entrancy guard string (e.g., contains `already running`), or is missing any issue IDs from ISSUES: treat as a **Triage agent failure**:
190
191
  - Retry the Triage agent once with the same inputs.
191
192
  - If the retry also fails the completeness check: abort with a clear error message listing the missing issue IDs and failure reason — never proceed with dropped issues.
@@ -199,7 +200,7 @@ Collect all decisions citations (ADR-NNN / PF-NNN) from Triage agent Reasoning c
199
200
 
200
201
  If FIX_NOW list is empty: skip to Phase 5 — write full summary (Phase 5), run manage-debt (Phase 9) if FIX_SEPARATE/TECH_DEBT exist, run thread resolution + resolution comment (Phase 9b), run merge readiness (Phase 9c), display results (Phase 10).
201
202
 
202
- Otherwise, batch FIX_NOW issues for Code agent execution:
203
+ Otherwise, batch FIX_NOW issues for Code agent execution. **DUPLICATE issues are never dispatched** — they inherit the primary's outcome:
203
204
  - **Same-file issues** → one batch (one Code agent per file, sequential for same-file pairs)
204
205
  - **Distinct-file issues** → parallel Code agents
205
206
  - **Max 5 issues per batch** — chunk large sets
@@ -207,7 +208,7 @@ Otherwise, batch FIX_NOW issues for Code agent execution:
207
208
  ### Phase 4: Fix (Code agent × N)
208
209
 
209
210
  **Produces:** CODE_AGENT_RESULTS
210
- **Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE, BRANCH_INFO
211
+ **Requires:** BATCHES, DECISIONS_CONTEXT, FEATURE_KNOWLEDGE
211
212
 
212
213
  For each batch, spawn Code agent with `OPERATION: issue-fix` and `PUSH: false`:
213
214
 
@@ -236,12 +237,14 @@ Collect from each Code agent:
236
237
  ### Phase 5: Write resolution-summary.md
237
238
 
238
239
  **Produces:** RESOLUTION_FILE (early write for compaction safety)
239
- **Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS
240
+ **Requires:** TRIAGE_RESULTS, CODE_AGENT_RESULTS, TARGET_DIR, BRANCH_INFO
240
241
 
241
242
  **Immediately write `resolution-summary.md`** to `\{TARGET_DIR\}` using the Write tool. Do this now — not in Phase 9 — while results are fresh in context. This ensures the record is persisted even if later phases (Simplify, Verification Gate, CI gate, Tech Debt) trigger context compaction.
242
243
 
243
244
  Set `Tracked` for FIX_SEPARATE and TECH_DEBT items to `(pending)` — to be backfilled after Phase 9 manage-debt.
244
245
 
246
+ DUPLICATE issues are listed **only** in `## Duplicates` — never in `## Fixed Issues`, `## False Positives`, `## By Design`, `## Fix Separately`, `## Deferred to Tech Debt`, `## Escalations`, or `## Blocked`. A duplicate of a FALSE_POSITIVE primary therefore leaves only the primary in the `False Positive` row and the `## False Positives` section; the same holds for every other outcome the duplicate inherits.
247
+
245
248
  Use the template from the Output Artifact section below.
246
249
 
247
250
  ### Phase 6: Simplify
@@ -334,7 +337,7 @@ Otherwise, for each worktree with fixes:
334
337
 
335
338
  **IMPORTANT**: Run sequentially across all worktrees (not in parallel) to avoid GitHub API conflicts.
336
339
 
337
- If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent:
340
+ If any issues are FIX_SEPARATE or TECH_DEBT, spawn Git agent. **DUPLICATE issues never create their own debt tickets** — a duplicate of a deferred primary is covered by the primary's ticket:
338
341
 
339
342
  ```
340
343
  Agent(subagent_type="Git"):
@@ -359,6 +362,7 @@ Skip this step if `COMPLIANCE_SKILL_INSTALLED` is false or THREAD_MAP is empty.
359
362
 
360
363
  Prepare THREAD_MAP with verdicts from triage/code agent results:
361
364
  - For each `ext-\{N\}`: match to an issue verdict (FIXED, FALSE_POSITIVE, BY_DESIGN, ESCALATED) by `file:line` correlation
365
+ - If the matched issue has verdict DUPLICATE, use the **primary's** verdict and verification status for the reply — do not expose DUPLICATE to the thread author (avoids PF-024; caller-side mapping — git.md contracts unchanged)
362
366
  - Include `commit_sha` from Code agent results for FIXED verdicts
363
367
  - Unmatched threads: ESCALATED (human review)
364
368
 
@@ -399,7 +403,7 @@ Update `## Third-Party Threads` section in resolution-summary.md with thread res
399
403
  ### Phase 9c: Merge Readiness (Compliance-gated, Report-only)
400
404
 
401
405
  **Produces:** MERGE_READINESS_REPORT
402
- **Requires:** PR_INFO, COMPLIANCE_SKILL_INSTALLED
406
+ **Requires:** BRANCH_INFO, COMPLIANCE_SKILL_INSTALLED
403
407
 
404
408
  Skip this phase if `COMPLIANCE_SKILL_INSTALLED` is false.
405
409
 
@@ -418,7 +422,7 @@ If Git agent returns `TRACEABILITY: DEGRADED`: warn, proceed to Phase 10.
418
422
 
419
423
  ### Phase 10: Report
420
424
 
421
- **Requires:** TARGET_DIR
425
+ **Requires:** BRANCH_INFO, TARGET_DIR
422
426
 
423
427
  The resolution summary was already written to `\{TARGET_DIR\}/resolution-summary.md` in Phase 5 (updated by Phase 7 and Phase 9). Display results to the user:
424
428
 
@@ -438,6 +442,7 @@ The resolution summary was already written to `\{TARGET_DIR\}/resolution-summary
438
442
  | Deferred | {n} |
439
443
  | Blocked | {n} |
440
444
  | Escalated | {n} |
445
+ | Duplicates Collapsed | {n} |
441
446
 
442
447
  ### Verification
443
448
  Final gate: {PASS | FAILED after N attempts}
@@ -483,9 +488,9 @@ In multi-worktree mode, report results per worktree with aggregate summary.
483
488
  │ └─ Git agent (fetch-review-threads) → THREAD_MAP
484
489
  │
485
490
  ├─ Phase 2: Global Triage [Triage agent, opus, single agent]
486
- │ └─ ALL issues → verdict ledger by disposition
491
+ │ └─ ALL issues → verdict ledger by disposition (incl. DUPLICATE with duplicate_of)
487
492
  │
488
- ├─ Phase 3: Batch FIX_NOW issues (skip if empty)
493
+ ├─ Phase 3: Batch FIX_NOW issues (skip if empty; DUPLICATE issues never dispatched)
489
494
  │ └─ same-file sequential, distinct-file parallel, max 5/batch
490
495
  │
491
496
  ├─ Phase 4: Fix [Code agent × N, OPERATION: issue-fix, PUSH: false]
@@ -528,6 +533,8 @@ In multi-worktree mode, report results per worktree with aggregate summary.
528
533
  | Worktree pre-flight fails | Report failure, continue with other worktrees |
529
534
  | Empty FIX_NOW list | Skip Phases 3-4/6-8; still write full summary + run manage-debt if FIX_SEPARATE/TECH_DEBT exist |
530
535
  | ESCALATED security issues | Surfaced in ## Escalations + display callout; never routed to manage-debt |
536
+ | DUPLICATE verdict without duplicate_of, or chained to another DUPLICATE | Treated as Triage failure — same retry-then-abort as a vanished id |
537
+ | DUPLICATE issues in THREAD_MAP | Map ext-\{N\} to primary's verdict/verification status for thread reply |
531
538
  | Verification Gate FAILED after 2 attempts | Recorded as FAILED in ## Verification + blocking callout; CI gate skipped; proceed to Phase 9 (manage-debt) then Phase 10 (display) |
532
539
  | gh/GitHub absent | manage-debt fails gracefully; Tracked stays "(pending)" + noted — recorded, not dropped |
533
540
  | COMPLIANCE_SKILL_INSTALLED false | Phases 1b, 9b-step-1, and 9c are skipped; post-resolution-summary (Phase 9b step 2) still runs if a PR is known |
@@ -556,7 +563,7 @@ Written in Phase 5 (Collect Results) to `\{TARGET_DIR\}/resolution-summary.md`:
556
563
  ```markdown
557
564
  # Resolution Summary
558
565
 
559
- **Branch**: {branch} -> {base}
566
+ **Branch**: {branch} -> {base_branch}
560
567
  **Date**: {timestamp}
561
568
  **Review**: {TARGET_DIR}
562
569
  **Command**: /resolve
@@ -578,8 +585,9 @@ Written in Phase 5 (Collect Results) to `\{TARGET_DIR\}/resolution-summary.md`:
578
585
  | Deferred | {n} |
579
586
  | Blocked | {n} |
580
587
  | Escalated | {n} |
588
+ | Duplicates Collapsed | {n} |
581
589
 
582
- _(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser.)_
590
+ _(Note: `Deferred` = `## Fix Separately` count + `## Deferred to Tech Debt` count combined — the two sections are distinct by scope, but the Statistics row aggregates both for the convergence parser. `Total Issues` counts every triaged issue including collapsed duplicates; every row **between** `Total Issues` and `Duplicates Collapsed` counts UNIQUE (non-DUPLICATE) issues only, so `Total Issues` equals the sum of the rows below it. Excluding duplicates from `Fixed`, `False Positive`, and `Deferred` de-skews the fp\_ratio convergence formula in code-review without any parser change.)_
583
591
 
584
592
  ## Verification
585
593
  | Command | Result |
@@ -625,6 +633,11 @@ Final gate: PASS | FAILED after {n} attempts
625
633
  |-------|-----------|---------|
626
634
  | {description} | {file}:{line} | {why} |
627
635
 
636
+ ## Duplicates
637
+ | Issue | Duplicate Of | File:Line |
638
+ |-------|-------------|-----------|
639
+ | {description} | {primary-id} | {file}:{line} |
640
+
628
641
  ## Third-Party Threads
629
642
  | Thread | File:Line | Verdict | Status |
630
643
  |--------|-----------|---------|--------|
@@ -633,4 +646,4 @@ Final gate: PASS | FAILED after {n} attempts
633
646
 
634
647
  _(Omit `## Third-Party Threads` if `COMPLIANCE_SKILL_INSTALLED` is false or no external threads were found.)_
635
648
 
636
- **Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `/code-review` convergence parser reads the `Deferred`, `Fixed`, and `False Positive` Statistics rows plus the `## Fixed Issues` / `## False Positives` headings — keep those labels byte-stable.
649
+ **Statistics mapping (parser contract)**: the `Deferred` row = FIX_SEPARATE + TECH_DEBT (both deferral dispositions combined); By Design and Escalated are counted separately and excluded from `Deferred`. The `Duplicates Collapsed` row is additive — the `/code-review` convergence parser reads only `Deferred`, `Fixed`, and `False Positive` rows plus `## Fixed Issues` / `## False Positives` headings — keep those labels byte-stable. All rows that the parser reads count UNIQUE (non-DUPLICATE) issues only, so collapsed duplicates do not inflate fp\_ratio.