@zigrivers/scaffold 3.27.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.
- package/content/knowledge/core/ai-memory-management.md +17 -0
- package/content/knowledge/core/claude-md-patterns.md +2 -2
- package/content/knowledge/core/coding-conventions.md +2 -2
- package/content/knowledge/core/task-decomposition.md +4 -4
- package/content/knowledge/core/task-tracking.md +120 -29
- package/content/knowledge/core/user-stories.md +1 -1
- package/content/knowledge/execution/multi-agent-coordination.md +118 -0
- package/content/knowledge/execution/task-claiming-strategy.md +15 -3
- package/content/knowledge/execution/worktree-management.md +5 -3
- package/content/pipeline/build/multi-agent-resume.md +27 -7
- package/content/pipeline/build/multi-agent-start.md +35 -7
- package/content/pipeline/build/new-enhancement.md +8 -1
- package/content/pipeline/build/quick-task.md +9 -0
- package/content/pipeline/build/single-agent-resume.md +11 -4
- package/content/pipeline/build/single-agent-start.md +13 -4
- package/content/pipeline/consolidation/workflow-audit.md +1 -1
- package/content/pipeline/environment/git-workflow.md +2 -2
- package/content/pipeline/foundation/beads.md +148 -22
- package/content/pipeline/foundation/coding-standards.md +1 -1
- package/content/tools/post-implementation-review.md +6 -6
- package/content/tools/prompt-pipeline.md +1 -1
- package/content/tools/release.md +5 -5
- package/content/tools/review-code.md +347 -3
- package/content/tools/review-pr.md +349 -7
- package/content/tools/version-bump.md +5 -5
- package/dist/cli/commands/observe.d.ts +2 -0
- package/dist/cli/commands/observe.d.ts.map +1 -1
- package/dist/cli/commands/observe.js +9 -1
- package/dist/cli/commands/observe.js.map +1 -1
- package/dist/cli/commands/observe.test.js +36 -0
- package/dist/cli/commands/observe.test.js.map +1 -1
- package/dist/observability/adapters/beads.d.ts +4 -0
- package/dist/observability/adapters/beads.d.ts.map +1 -1
- package/dist/observability/adapters/beads.js +25 -2
- package/dist/observability/adapters/beads.js.map +1 -1
- package/dist/observability/adapters/beads.test.js +40 -2
- package/dist/observability/adapters/beads.test.js.map +1 -1
- package/dist/observability/engine/ledger-writer.d.ts +11 -1
- package/dist/observability/engine/ledger-writer.d.ts.map +1 -1
- package/dist/observability/engine/ledger-writer.js +6 -0
- package/dist/observability/engine/ledger-writer.js.map +1 -1
- package/dist/observability/engine/llm-dispatcher.d.ts.map +1 -1
- package/dist/observability/engine/llm-dispatcher.js +36 -5
- package/dist/observability/engine/llm-dispatcher.js.map +1 -1
- package/dist/observability/engine/llm-dispatcher.test.js +23 -0
- package/dist/observability/engine/llm-dispatcher.test.js.map +1 -1
- 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
|
-
"
|
|
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
|
|
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).
|