@zigrivers/scaffold 3.26.0 → 3.28.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.
Files changed (126) hide show
  1. package/README.md +21 -7
  2. package/content/knowledge/core/ai-memory-management.md +17 -0
  3. package/content/knowledge/core/claude-md-patterns.md +2 -2
  4. package/content/knowledge/core/coding-conventions.md +2 -2
  5. package/content/knowledge/core/task-decomposition.md +4 -4
  6. package/content/knowledge/core/task-tracking.md +120 -29
  7. package/content/knowledge/core/user-stories.md +1 -1
  8. package/content/knowledge/execution/multi-agent-coordination.md +118 -0
  9. package/content/knowledge/execution/task-claiming-strategy.md +15 -3
  10. package/content/knowledge/execution/worktree-management.md +5 -3
  11. package/content/knowledge/web3/web3-access-control.md +189 -0
  12. package/content/knowledge/web3/web3-architecture.md +162 -0
  13. package/content/knowledge/web3/web3-audit-workflow.md +151 -0
  14. package/content/knowledge/web3/web3-common-vulnerabilities.md +171 -0
  15. package/content/knowledge/web3/web3-conventions.md +162 -0
  16. package/content/knowledge/web3/web3-deployment-and-verification.md +216 -0
  17. package/content/knowledge/web3/web3-dev-environment.md +150 -0
  18. package/content/knowledge/web3/web3-gas-optimization.md +165 -0
  19. package/content/knowledge/web3/web3-oracles-and-external-data.md +155 -0
  20. package/content/knowledge/web3/web3-project-structure.md +212 -0
  21. package/content/knowledge/web3/web3-requirements.md +152 -0
  22. package/content/knowledge/web3/web3-security.md +163 -0
  23. package/content/knowledge/web3/web3-testing.md +180 -0
  24. package/content/knowledge/web3/web3-upgradeability.md +189 -0
  25. package/content/methodology/web3-overlay.yml +40 -0
  26. package/content/pipeline/build/multi-agent-resume.md +27 -7
  27. package/content/pipeline/build/multi-agent-start.md +35 -7
  28. package/content/pipeline/build/new-enhancement.md +8 -1
  29. package/content/pipeline/build/quick-task.md +9 -0
  30. package/content/pipeline/build/single-agent-resume.md +11 -4
  31. package/content/pipeline/build/single-agent-start.md +13 -4
  32. package/content/pipeline/consolidation/workflow-audit.md +1 -1
  33. package/content/pipeline/environment/git-workflow.md +2 -2
  34. package/content/pipeline/foundation/beads.md +148 -22
  35. package/content/pipeline/foundation/coding-standards.md +1 -1
  36. package/content/tools/post-implementation-review.md +6 -6
  37. package/content/tools/prompt-pipeline.md +1 -1
  38. package/content/tools/release.md +5 -5
  39. package/content/tools/review-code.md +347 -3
  40. package/content/tools/review-pr.md +349 -7
  41. package/content/tools/version-bump.md +5 -5
  42. package/dist/cli/commands/observe.d.ts +2 -0
  43. package/dist/cli/commands/observe.d.ts.map +1 -1
  44. package/dist/cli/commands/observe.js +9 -1
  45. package/dist/cli/commands/observe.js.map +1 -1
  46. package/dist/cli/commands/observe.test.js +36 -0
  47. package/dist/cli/commands/observe.test.js.map +1 -1
  48. package/dist/config/schema.d.ts +672 -126
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/config/schema.js +8 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/schema.test.js +2 -2
  53. package/dist/config/schema.test.js.map +1 -1
  54. package/dist/config/validators/index.d.ts.map +1 -1
  55. package/dist/config/validators/index.js +2 -0
  56. package/dist/config/validators/index.js.map +1 -1
  57. package/dist/config/validators/web3.d.ts +4 -0
  58. package/dist/config/validators/web3.d.ts.map +1 -0
  59. package/dist/config/validators/web3.js +15 -0
  60. package/dist/config/validators/web3.js.map +1 -0
  61. package/dist/e2e/project-type-overlays.test.js +76 -0
  62. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  63. package/dist/observability/adapters/beads.d.ts +4 -0
  64. package/dist/observability/adapters/beads.d.ts.map +1 -1
  65. package/dist/observability/adapters/beads.js +25 -2
  66. package/dist/observability/adapters/beads.js.map +1 -1
  67. package/dist/observability/adapters/beads.test.js +40 -2
  68. package/dist/observability/adapters/beads.test.js.map +1 -1
  69. package/dist/observability/engine/ledger-writer.d.ts +11 -1
  70. package/dist/observability/engine/ledger-writer.d.ts.map +1 -1
  71. package/dist/observability/engine/ledger-writer.js +6 -0
  72. package/dist/observability/engine/ledger-writer.js.map +1 -1
  73. package/dist/observability/engine/llm-dispatcher.d.ts.map +1 -1
  74. package/dist/observability/engine/llm-dispatcher.js +36 -5
  75. package/dist/observability/engine/llm-dispatcher.js.map +1 -1
  76. package/dist/observability/engine/llm-dispatcher.test.js +23 -0
  77. package/dist/observability/engine/llm-dispatcher.test.js.map +1 -1
  78. package/dist/project/adopt.d.ts.map +1 -1
  79. package/dist/project/adopt.js +3 -1
  80. package/dist/project/adopt.js.map +1 -1
  81. package/dist/project/detectors/coverage.test.js +3 -2
  82. package/dist/project/detectors/coverage.test.js.map +1 -1
  83. package/dist/project/detectors/disambiguate.js +1 -1
  84. package/dist/project/detectors/disambiguate.js.map +1 -1
  85. package/dist/project/detectors/index.d.ts.map +1 -1
  86. package/dist/project/detectors/index.js +2 -0
  87. package/dist/project/detectors/index.js.map +1 -1
  88. package/dist/project/detectors/resolve-detection.test.js +57 -0
  89. package/dist/project/detectors/resolve-detection.test.js.map +1 -1
  90. package/dist/project/detectors/types.d.ts +6 -2
  91. package/dist/project/detectors/types.d.ts.map +1 -1
  92. package/dist/project/detectors/types.js.map +1 -1
  93. package/dist/project/detectors/web3.d.ts +4 -0
  94. package/dist/project/detectors/web3.d.ts.map +1 -0
  95. package/dist/project/detectors/web3.js +37 -0
  96. package/dist/project/detectors/web3.js.map +1 -0
  97. package/dist/project/detectors/web3.test.d.ts +2 -0
  98. package/dist/project/detectors/web3.test.d.ts.map +1 -0
  99. package/dist/project/detectors/web3.test.js +75 -0
  100. package/dist/project/detectors/web3.test.js.map +1 -0
  101. package/dist/types/config.d.ts +8 -1
  102. package/dist/types/config.d.ts.map +1 -1
  103. package/dist/wizard/copy/core.d.ts.map +1 -1
  104. package/dist/wizard/copy/core.js +4 -0
  105. package/dist/wizard/copy/core.js.map +1 -1
  106. package/dist/wizard/copy/index.d.ts.map +1 -1
  107. package/dist/wizard/copy/index.js +2 -0
  108. package/dist/wizard/copy/index.js.map +1 -1
  109. package/dist/wizard/copy/types.d.ts +5 -1
  110. package/dist/wizard/copy/types.d.ts.map +1 -1
  111. package/dist/wizard/copy/types.test-d.js +7 -0
  112. package/dist/wizard/copy/types.test-d.js.map +1 -1
  113. package/dist/wizard/copy/web3.d.ts +3 -0
  114. package/dist/wizard/copy/web3.d.ts.map +1 -0
  115. package/dist/wizard/copy/web3.js +15 -0
  116. package/dist/wizard/copy/web3.js.map +1 -0
  117. package/dist/wizard/questions.d.ts +2 -1
  118. package/dist/wizard/questions.d.ts.map +1 -1
  119. package/dist/wizard/questions.js +8 -1
  120. package/dist/wizard/questions.js.map +1 -1
  121. package/dist/wizard/questions.test.js +14 -0
  122. package/dist/wizard/questions.test.js.map +1 -1
  123. package/dist/wizard/wizard.d.ts.map +1 -1
  124. package/dist/wizard/wizard.js +1 -0
  125. package/dist/wizard/wizard.js.map +1 -1
  126. package/package.json +1 -1
@@ -133,7 +133,7 @@ echo "$AGENT_FINDINGS" > /tmp/agent-findings.json
133
133
  mmr reconcile "$JOB_ID" --channel superpowers --input /tmp/agent-findings.json
134
134
  ```
135
135
 
136
- The agent's review output must use MMR-compatible finding schema: each finding needs `severity` (P0-P3), `location` (file:line), and `description` (`suggestion` is optional).
136
+ The agent's review output must use MMR-compatible finding schema: each finding needs `severity` (P0-P3), `category` (correctness, edge-case, security, acceptance-criteria, test-coverage, or another stable category), `location` (file:line), and `description` (`suggestion` is optional).
137
137
 
138
138
  If `mmr` is not installed (`command -v mmr` fails), fall back to the manual multi-channel flow below.
139
139
 
@@ -363,7 +363,8 @@ Respond with JSON:
363
363
  "findings": [
364
364
  {
365
365
  "severity": "P0" | "P1" | "P2" | "P3",
366
- "location": "file:line or section",
366
+ "category": "correctness|edge-case|security|acceptance-criteria|test-coverage",
367
+ "location": "relative/path/to/file.ts:42",
367
368
  "description": "what is wrong",
368
369
  "suggestion": "specific fix"
369
370
  }
@@ -397,10 +398,352 @@ Otherwise:
397
398
  1. Fix all findings at or above `fix_threshold` (read from `results.fix_threshold` in the verdict JSON; default `P2`)
398
399
  2. Re-run the channels that produced findings
399
400
  3. Keep iterating as long as each new round surfaces *different, concrete, fixable* findings — that is healthy review/fix iteration, not a stuck loop
400
- 4. The 3-round limit is **per finding**: stop and surface to the user when the *same* blocking finding (or set) recurs across 3 attempts without progress. Other stop conditions: a finding is genuinely ambiguous (channels contradict each other), or the user explicitly asks to stop. Use verdict `needs-user-decision` for ambiguity, `blocked` for stuck-loop cases.
401
+ 4. The 3-round limit is **per finding hash**, enforced by the wrapper-side bookkeeping in Step 7a (`.scaffold/review-attempts/<session-id>.json`). Stop and surface to the user when any blocking finding's hash hits 3 attempts (`_review_at_strike_limit` returns true). Other stop conditions: a finding is genuinely ambiguous (channels contradict each other), or the user explicitly asks to stop. Use verdict `needs-user-decision` for ambiguity, `blocked` for stuck-hash cases. Identity components — `location`, `category`, `description`, `suggestion` — mirror MMR T2-A's forthcoming native `finding_key` (v3.30).
401
402
 
402
403
  **Fix cycle channel rule:** Re-run only channels that originally completed or ran as compensating passes. Never retry a channel marked `not_installed`, `auth_failed`, or `timeout` during fix rounds — its availability does not change within a session.
403
404
 
405
+ ### Step 7a: Wrapper-Side Per-Finding Hash (Stopgap until MMR v3.30)
406
+
407
+ Same wrapper-side bookkeeping as `review-pr.md` Step 7a. Local review reuses
408
+ the helper semantics, with local-session derivation (no PR number) and the
409
+ portable hardening noted below.
410
+
411
+ This section is throwaway — when MMR v3.30 lands, replace this entire block
412
+ with `mmr review --session <id> --max-rounds N` and read `finding_key` from
413
+ the verdict JSON directly.
414
+
415
+ #### Session id (review-code variant)
416
+
417
+ ```bash
418
+ _review_session_id() {
419
+ _review_sanitize_session_id() {
420
+ local raw="$1" sanitized
421
+ sanitized=$(printf '%s' "$raw" | tr -c 'a-zA-Z0-9_.-' '_')
422
+ if [ -z "$sanitized" ] || [ "$sanitized" = "." ] || [ "$sanitized" = ".." ]; then
423
+ echo "Error: review session id resolves to an unsafe path segment" >&2
424
+ return 1
425
+ fi
426
+ printf '%s' "$sanitized"
427
+ }
428
+
429
+ if [ -n "${REVIEW_SESSION_ID:-}" ]; then
430
+ _review_sanitize_session_id "$REVIEW_SESSION_ID" || return 1
431
+ return
432
+ fi
433
+ if [ -n "${__REVIEW_SESSION_ID:-}" ]; then
434
+ _review_sanitize_session_id "$__REVIEW_SESSION_ID" || return 1
435
+ return
436
+ fi
437
+ local branch base
438
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
439
+ base="${BASE_REF:-main}"
440
+ if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then
441
+ __REVIEW_SESSION_ID=$(_review_sanitize_session_id "$branch@$base") || return 1
442
+ printf '%s' "$__REVIEW_SESSION_ID"
443
+ return
444
+ fi
445
+ local commit
446
+ commit=$(git rev-parse --short HEAD 2>/dev/null || echo "")
447
+ if [ -n "$commit" ]; then
448
+ __REVIEW_SESSION_ID=$(_review_sanitize_session_id "commit-$commit") || return 1
449
+ printf '%s' "$__REVIEW_SESSION_ID"
450
+ return
451
+ fi
452
+ __REVIEW_SESSION_ID=$(_review_sanitize_session_id "ts-$(date -u +%Y-%m-%dT%H:%M:%SZ)") || return 1
453
+ printf '%s' "$__REVIEW_SESSION_ID"
454
+ }
455
+
456
+ _review_attempts_file() {
457
+ local id; id=$(_review_session_id) || return 1
458
+ mkdir -p .scaffold/review-attempts || return 1
459
+ printf '.scaffold/review-attempts/%s.json' "$id"
460
+ }
461
+ ```
462
+
463
+ #### Normalization, hashing, shingle, and attempt-recording helpers
464
+
465
+ The functions `_review_normalize_location`, `_review_normalize_description`,
466
+ `_review_normalize_suggestion`, `_review_finding_hash`,
467
+ `_review_description_shingle`, `_review_record_attempt`, and
468
+ `_review_at_strike_limit` match the semantics of the ones defined in
469
+ `content/tools/review-pr.md` Step 7a. They are reproduced here so this file is
470
+ self-contained:
471
+
472
+ They are intentionally reproduced in this tool file instead of sourced from a
473
+ shared script because agent-facing tool markdown must be self-contained until
474
+ MMR v3.30 provides native `finding_key` and session tracking.
475
+
476
+ The local-review variant also makes dependency checks explicit, preserves
477
+ backtick code spans in suggestions, and keeps short-description shingles useful
478
+ with shorter n-grams.
479
+
480
+ ```bash
481
+ _review_require_jq() {
482
+ command -v jq >/dev/null 2>&1 || {
483
+ echo "Error: jq is required for review finding bookkeeping" >&2
484
+ return 1
485
+ }
486
+ }
487
+
488
+ _review_require_python3() {
489
+ command -v python3 >/dev/null 2>&1 || {
490
+ echo "Error: python3 is required for review finding normalization" >&2
491
+ return 1
492
+ }
493
+ }
494
+
495
+ _review_sha1() {
496
+ if command -v shasum >/dev/null 2>&1; then
497
+ shasum -a 1 | awk '{print $1}'
498
+ elif command -v sha1sum >/dev/null 2>&1; then
499
+ sha1sum | awk '{print $1}'
500
+ else
501
+ echo "Error: shasum or sha1sum is required for review finding hashing" >&2
502
+ return 1
503
+ fi
504
+ }
505
+
506
+ _review_normalize_location() {
507
+ # Input: $1 = raw location (e.g. "src/foo.ts:42-44" or "pkg/Bar.kt (line 10)")
508
+ # Output: lowercased file path with trailing :N, :N-M, :N:M, (line N) stripped
509
+ printf '%s' "$1" \
510
+ | tr '[:upper:]' '[:lower:]' \
511
+ | awk '{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, ""); print }' \
512
+ | sed -E 's/(:[0-9]+(:[0-9]+)?(-[0-9]+)?|[[:space:]]+\(line[[:space:]]+[0-9]+\))$//'
513
+ }
514
+
515
+ _review_normalize_description() {
516
+ # Input: $1 = raw description
517
+ # Output: tokenize on backticks → normalize non-code segments → reassemble
518
+ _review_require_python3 || return 1
519
+ printf '%s' "$1" | python3 -c '
520
+ import re, sys
521
+ s = sys.stdin.read()
522
+ out = []
523
+ pattern = re.compile(r"(```[\s\S]*?```|`[^`]*`)")
524
+ pos = 0
525
+ for match in pattern.finditer(s):
526
+ seg = s[pos:match.start()]
527
+ seg = seg.lower()
528
+ seg = re.sub(r"\bline\s+\d+\b", "", seg)
529
+ seg = re.sub(r"\bat\s+line\s+\d+\b", "", seg)
530
+ seg = re.sub(r"^\s*(p[0-3]|critical|high|medium|low|trivial)\s*:\s*", "", seg)
531
+ seg = re.sub(r"\s+", " ", seg).strip()
532
+ if seg:
533
+ out.append(seg)
534
+ out.append(match.group(0))
535
+ pos = match.end()
536
+ seg = s[pos:]
537
+ seg = seg.lower()
538
+ seg = re.sub(r"\bline\s+\d+\b", "", seg)
539
+ seg = re.sub(r"\bat\s+line\s+\d+\b", "", seg)
540
+ seg = re.sub(r"^\s*(p[0-3]|critical|high|medium|low|trivial)\s*:\s*", "", seg)
541
+ seg = re.sub(r"\s+", " ", seg).strip()
542
+ if seg:
543
+ out.append(seg)
544
+ print(" ".join(p for p in out if p))
545
+ '
546
+ }
547
+
548
+ _review_normalize_suggestion() {
549
+ _review_require_python3 || return 1
550
+ printf '%s' "$1" | python3 -c '
551
+ import re, sys
552
+ s = sys.stdin.read()
553
+ out = []
554
+ pattern = re.compile(r"(```[\s\S]*?```|`[^`]*`)")
555
+ pos = 0
556
+ for match in pattern.finditer(s):
557
+ seg = re.sub(r"\s+", " ", s[pos:match.start()].lower()).strip()
558
+ if seg:
559
+ out.append(seg)
560
+ out.append(match.group(0))
561
+ pos = match.end()
562
+ seg = re.sub(r"\s+", " ", s[pos:].lower()).strip()
563
+ if seg:
564
+ out.append(seg)
565
+ print(" ".join(p for p in out if p))
566
+ '
567
+ }
568
+
569
+ _review_finding_hash() {
570
+ # Input: $1 = single-finding JSON object (with location, category, description, suggestion fields)
571
+ # Output: 40-char sha1 hex of normalized_location + "|" + category + "|" + sha1(description_normalized) + "|" + sha1(suggestion_normalized)
572
+ _review_require_jq || return 1
573
+ local f="$1"
574
+ local loc cat desc sugg
575
+ loc=$(printf '%s' "$f" | jq -r '.location // ""')
576
+ cat=$(printf '%s' "$f" | jq -r '.category // ""')
577
+ desc=$(printf '%s' "$f" | jq -r '.description // ""')
578
+ sugg=$(printf '%s' "$f" | jq -r '.suggestion // ""')
579
+
580
+ local nloc ndesc nsugg dhash shash
581
+ nloc=$(_review_normalize_location "$loc") || return 1
582
+ ndesc=$(_review_normalize_description "$desc") || return 1
583
+ nsugg=$(_review_normalize_suggestion "$sugg") || return 1
584
+ dhash=$(printf '%s' "$ndesc" | _review_sha1) || return 1
585
+ shash=$(printf '%s' "$nsugg" | _review_sha1) || return 1
586
+
587
+ printf '%s|%s|%s|%s' "$nloc" "$cat" "$dhash" "$shash" \
588
+ | _review_sha1
589
+ }
590
+
591
+ _review_description_shingle() {
592
+ # Input: $1 = normalized description
593
+ # Output: JSON array of normalized 5-grams (token-based)
594
+ _review_require_python3 || return 1
595
+ printf '%s' "$1" | python3 -c '
596
+ import json, sys
597
+ tokens = sys.stdin.read().split()
598
+ n = min(5, len(tokens))
599
+ shingles = [] if n == 0 else sorted({" ".join(tokens[i:i+n]) for i in range(len(tokens)-n+1)})
600
+ print(json.dumps(shingles))
601
+ '
602
+ }
603
+
604
+ _review_record_attempt() {
605
+ # Input: $1 = finding JSON, $2 = current round number (1-based), $3 = optional precomputed finding hash
606
+ # Side effect: increments attempts in the attempts file
607
+ # Output: prints new attempt count on stdout
608
+ _review_require_jq || return 1
609
+ local f="$1" round="$2" hash="${3:-}"
610
+ local file loc desc nloc ndesc shingle
611
+ file=$(_review_attempts_file)
612
+ loc=$(printf '%s' "$f" | jq -r '.location // ""')
613
+ desc=$(printf '%s' "$f" | jq -r '.description // ""')
614
+ nloc=$(_review_normalize_location "$loc") || return 1
615
+ ndesc=$(_review_normalize_description "$desc") || return 1
616
+ if [ -z "$hash" ]; then
617
+ hash=$(_review_finding_hash "$f") || return 1
618
+ fi
619
+ shingle=$(_review_description_shingle "$ndesc") || return 1
620
+
621
+ [ -f "$file" ] || jq -n --arg id "$(_review_session_id)" --arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
622
+ '{session_id: $id, created_at: $created, findings: {}}' > "$file"
623
+
624
+ jq --arg h "$hash" --arg loc "$nloc" --argjson sh "$shingle" --argjson r "$round" '
625
+ .findings[$h] = (
626
+ .findings[$h] // {attempts: 0, first_seen_round: $r, normalized_location: $loc, description_shingle: $sh}
627
+ | .attempts += (if .last_seen_round == $r then 0 else 1 end)
628
+ | .last_seen_round = $r
629
+ )
630
+ ' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
631
+
632
+ jq -r --arg h "$hash" '.findings[$h].attempts' "$file"
633
+ }
634
+
635
+ _review_at_strike_limit() {
636
+ # Input: $1 = finding JSON, $2 = optional precomputed finding hash
637
+ # Exit: 0 if hash already has >= REVIEW_STRIKE_LIMIT attempts, 1 otherwise
638
+ _review_require_jq || return 1
639
+ local f="$1" file hash
640
+ hash="${2:-}"
641
+ file=$(_review_attempts_file)
642
+ [ -f "$file" ] || return 1
643
+ [ -n "$hash" ] || hash=$(_review_finding_hash "$f") || return 1
644
+ local n; n=$(jq -r --arg h "$hash" '.findings[$h].attempts // 0' "$file")
645
+ [ "$n" -ge "${REVIEW_STRIKE_LIMIT:-3}" ]
646
+ }
647
+ ```
648
+
649
+ This bookkeeping assumes sequential execution within a single workspace or
650
+ worktree. Do not run multiple review/fix loops against the same
651
+ `REVIEW_SESSION_ID` concurrently.
652
+
653
+ #### Per-round flow
654
+
655
+ After each `mmr review …` call (or `mmr results "$JOB_ID"` for the manual
656
+ fallback), iterate the reconciled findings at or above `fix_threshold`. For each
657
+ blocking finding, compute its hash, call `_review_at_strike_limit "$f" "$hash"`
658
+ before incrementing, and stop the fix loop with verdict `blocked` per Step 8 if
659
+ the hash is already at `${REVIEW_STRIKE_LIMIT:-3}` recorded attempts. Otherwise,
660
+ call `_review_record_attempt "$f" "$ROUND" "$hash"` to record the current
661
+ round before applying the next fix.
662
+
663
+ For very noisy fix loops you may suggest `--fix-threshold P1` to narrow the
664
+ gate; the project default stays at P2 per the design's Decision 4. Do not
665
+ auto-change the threshold.
666
+
667
+ Identity components — `location`, `category`, `description`, and `suggestion`
668
+ — mirror MMR T2-A's forthcoming native `finding_key` so this remains a clean
669
+ migration when v3.30 ships.
670
+
671
+ ### Step 7b: File blocking findings as Beads tasks (opt-in)
672
+
673
+ If `.mmr.yaml` has `beads.create_issues_from_blocking_findings: true` AND `.beads/`
674
+ exists, file each blocking finding (severity at-or-above `beads.fix_threshold`,
675
+ default `P2`) as a Beads bug. This is purely additive tracking — it does NOT replace
676
+ Step 7's fix-in-place flow; it creates a durable record of findings that ought to
677
+ become standalone follow-up work.
678
+
679
+ ```bash
680
+ # First: gate on the opt-in flag in .mmr.yaml. Defaults to disabled.
681
+ # Uses pure bash + grep/sed — no yq dependency.
682
+ beads_enabled=false
683
+ beads_fix_threshold=P2
684
+ beads_default_type=bug
685
+ if [ -f .mmr.yaml ]; then
686
+ # POSIX character classes ([[:space:]]) for BSD-sed compatibility (macOS default).
687
+ # Patterns tolerate trailing whitespace/comments — uncommenting a template line with
688
+ # a trailing `# comment` should still match.
689
+ if grep -qE '^[[:space:]]*create_issues_from_blocking_findings:[[:space:]]*true([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml; then
690
+ beads_enabled=true
691
+ fi
692
+ if v=$(grep -E '^[[:space:]]*fix_threshold:[[:space:]]*P[0-4]([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml | head -1 | sed -E 's/^[^:]*:[[:space:]]*(P[0-4]).*/\1/'); [ -n "$v" ]; then
693
+ beads_fix_threshold=$v
694
+ fi
695
+ if v=$(grep -E '^[[:space:]]*default_type:[[:space:]]*[a-zA-Z]+([[:space:]]+#.*)?[[:space:]]*$' .mmr.yaml | head -1 | sed -E 's/^[^:]*:[[:space:]]*([a-zA-Z]+).*/\1/'); [ -n "$v" ]; then
696
+ beads_default_type=$v
697
+ fi
698
+ fi
699
+
700
+ if [ "$beads_enabled" = "true" ] && [ -d .beads ] && command -v bd >/dev/null 2>&1 \
701
+ && command -v mmr >/dev/null 2>&1 && [ -n "${JOB_ID:-}" ]; then
702
+ # Skip when the review-code flow ran in manual fallback mode (no mmr, no JOB_ID).
703
+ threshold_rank=$(case "$beads_fix_threshold" in P0) echo 0;; P1) echo 1;; P2) echo 2;; P3) echo 3;; *) echo 4;; esac)
704
+
705
+ # Capture the reconciled findings from the MMR job we already ran upstream.
706
+ # MMR JSON shape: { reconciled_findings: [{ severity, location, description, suggestion, ... }] }
707
+ review_json=$(mmr results "$JOB_ID" --format json)
708
+
709
+ while IFS= read -r finding; do
710
+ title=$(jq -r '.description | .[0:120]' <<<"$finding")
711
+ severity=$(jq -r '.severity' <<<"$finding")
712
+ pnum="${severity#P}"
713
+ description=$(jq -r --arg job "$JOB_ID" '"\(.description)\n\nSuggestion: \(.suggestion // "(none)")\n\nLocation: \(.location // "(unknown)")\n\nFirst seen in MMR job: \($job)"' <<<"$finding")
714
+ # Per-finding identity for a future dedupe-on-re-runs mechanism (matches
715
+ # review-pr.md Step 7b). NOTE: bd v1.0.4 has no `bd list --external-ref`
716
+ # flag, so the bridge does not enforce dedupe at write time — known
717
+ # limitation; same-job re-runs will create duplicates.
718
+ loc=$(jq -r '.location // ""' <<<"$finding")
719
+ desc_for_hash=$(jq -r '.description // ""' <<<"$finding")
720
+ finding_hash=$(printf '%s|%s' "$loc" "$desc_for_hash" | shasum -a 1 | cut -c1-8)
721
+
722
+ args=(
723
+ "$title"
724
+ --type "$beads_default_type"
725
+ -p "$pnum"
726
+ --description "$description"
727
+ --external-ref "mmr:$finding_hash"
728
+ )
729
+ if [ -n "${SOURCE_BD_ID:-}" ]; then
730
+ args+=(--deps "discovered-from:$SOURCE_BD_ID")
731
+ fi
732
+ bd create "${args[@]}"
733
+ done < <(jq -c --argjson maxRank "$threshold_rank" '
734
+ .reconciled_findings[]?
735
+ | (.severity | sub("^P";"") | tonumber) as $rank
736
+ | select($rank <= $maxRank)
737
+ ' <<<"$review_json")
738
+ fi
739
+ ```
740
+
741
+ Same shell idioms as `review-pr.md` Step 7b — see that file for notes on the jq
742
+ arguments and UTF-8-safe truncation. The `.mmr.yaml` opt-in flag is read first;
743
+ the rest of the block is skipped unless `beads.create_issues_from_blocking_findings`
744
+ is `true`. The `--deps discovered-from:$SOURCE_BD_ID` flag is conditional on a
745
+ non-empty `$SOURCE_BD_ID`.
746
+
404
747
  ### Step 8: Final Verdict
405
748
 
406
749
  Return exactly one verdict:
@@ -446,3 +789,4 @@ for the next delivery step (commit, push, or PR creation).
446
789
  4. **Independence** — never share one channel's output with another.
447
790
  5. **Fix before proceeding** — findings at or above `fix_threshold` must be resolved before moving to the next task.
448
791
  6. **Dispatch pattern** follows `multi-model-review-dispatch` knowledge entry. When modifying channel dispatch in this file, verify consistency with `review-pr.md` and `post-implementation-review.md`.
792
+ 7. **3-round limit (per finding hash)** — never attempt to fix the *same* blocking finding (identified by the Step 7a hash of `location` + `category` + `description` + `suggestion`) more than 3 times. The attempts file `.scaffold/review-attempts/<session-id>.json` is the exact-hash source of truth; `_review_at_strike_limit` checks it. Description and suggestion are intentionally part of the strict hash to mirror MMR T2-A's forthcoming native `finding_key`; the same-underlying-defect stop condition is a co-equal guard for wording jitter that produces new hashes. Each round that surfaces genuinely different findings with *new* hashes is healthy iteration — keep going. Stop when a hash hits 3 attempts, when the same underlying defect recurs across 3 rounds even if reviewer wording produces new hashes, when channels contradict each other, or when the user asks to stop. For noisy fix loops, optionally suggest `--fix-threshold P1` (the project default stays at P2).