forgedock 1.1.7 → 1.1.8
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/bin/engine/invariants.mjs +361 -0
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +540 -0
- package/commands/autopilot.md +957 -76
- package/commands/cleanup.md +52 -3
- package/commands/optimize.md +585 -0
- package/commands/orchestrate/phase-1-resolve.md +35 -25
- package/commands/orchestrate/phase-3-dependency.md +465 -34
- package/commands/orchestrate/phase-4-execution.md +447 -28
- package/commands/orchestrate/phase-6-report.md +198 -9
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/quality-gate.md +417 -0
- package/commands/review-pr.md +391 -13
- package/commands/spec-doctor.md +429 -0
- package/commands/validate.md +52 -0
- package/commands/work-on/build/architect.md +77 -0
- package/commands/work-on/build/context.md +357 -22
- package/commands/work-on/build/implement.md +2 -0
- package/commands/work-on/build/validate.md +28 -0
- package/commands/work-on/build.md +44 -0
- package/commands/work-on/close.md +621 -8
- package/commands/work-on/investigate.md +221 -0
- package/commands/work-on.md +15 -14
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +2 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/danger-zones.mjs +782 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +157 -6
package/commands/cleanup.md
CHANGED
|
@@ -91,11 +91,58 @@ done
|
|
|
91
91
|
- If it has `workflow:invalid` already → just remove `workflow:investigating`
|
|
92
92
|
- If closed normally → add `workflow:merged`, remove `workflow:investigating`
|
|
93
93
|
|
|
94
|
-
**Stale `needs-validation`** —
|
|
94
|
+
**Stale `needs-validation`** — backfill from FORGE:INVESTIGATOR verdict where available; strip-only for issues with no verdict comment: <!-- Added: forge#1730 -->
|
|
95
|
+
|
|
96
|
+
This is the H1 backfill sweep from the Design Decision (#1730). For each closed finding-issue with `needs-validation`:
|
|
97
|
+
1. Fetch any `FORGE:INVESTIGATOR` comment on the issue
|
|
98
|
+
2. Extract the `**Verdict**: ...` line
|
|
99
|
+
3. `CONFIRMED` → apply `validated` + remove `needs-validation`
|
|
100
|
+
4. `NOT-CONFIRMED` / `INVALID` / `PARTIAL` → apply `false-positive` + remove `needs-validation`
|
|
101
|
+
5. No verdict comment (or unrecognized verdict) → strip `needs-validation` only (no label added — do not guess)
|
|
102
|
+
|
|
103
|
+
**H4 is forbidden**: Do NOT use file-overlap heuristics (whether a fix-PR touched the cited file) to infer validated/false-positive. H4 has ~10% error rate and poisons the corpus. If there is no FORGE:INVESTIGATOR comment, the issue goes to strip-only.
|
|
104
|
+
|
|
95
105
|
```bash
|
|
106
|
+
BACKFILL_VALIDATED=0
|
|
107
|
+
BACKFILL_FALSE_POSITIVE=0
|
|
108
|
+
BACKFILL_STRIP_ONLY=0
|
|
109
|
+
|
|
96
110
|
for NUM in {needs_validation_numbers}; do
|
|
97
|
-
|
|
111
|
+
# Fetch FORGE:INVESTIGATOR comment if any
|
|
112
|
+
INVESTIGATOR_COMMENT=$(gh api repos/{GH_REPO}/issues/$NUM/comments \
|
|
113
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null || echo "")
|
|
114
|
+
|
|
115
|
+
if [ -n "$INVESTIGATOR_COMMENT" ]; then
|
|
116
|
+
# Extract verdict — matches "**Verdict**: CONFIRMED" etc.
|
|
117
|
+
VERDICT=$(echo "$INVESTIGATOR_COMMENT" | grep -oE '\*\*Verdict\*\*: [A-Za-z0-9_][A-Za-z0-9_-]*' | sed 's/^\*\*Verdict\*\*: //' | head -1)
|
|
118
|
+
|
|
119
|
+
case "$VERDICT" in
|
|
120
|
+
CONFIRMED)
|
|
121
|
+
gh issue edit $NUM {GH_FLAG} --add-label "validated" --remove-label "needs-validation" 2>/dev/null || true
|
|
122
|
+
echo "#$NUM: needs-validation → validated (verdict: CONFIRMED)"
|
|
123
|
+
BACKFILL_VALIDATED=$((BACKFILL_VALIDATED + 1))
|
|
124
|
+
;;
|
|
125
|
+
NOT-CONFIRMED|INVALID|PARTIAL)
|
|
126
|
+
gh issue edit $NUM {GH_FLAG} --add-label "false-positive" --remove-label "needs-validation" 2>/dev/null || true
|
|
127
|
+
echo "#$NUM: needs-validation → false-positive (verdict: $VERDICT)"
|
|
128
|
+
BACKFILL_FALSE_POSITIVE=$((BACKFILL_FALSE_POSITIVE + 1))
|
|
129
|
+
;;
|
|
130
|
+
*)
|
|
131
|
+
# Unrecognized or empty verdict — strip-only (no guessing)
|
|
132
|
+
gh issue edit $NUM {GH_FLAG} --remove-label "needs-validation" 2>/dev/null || true
|
|
133
|
+
echo "#$NUM: needs-validation stripped (no recognized verdict in INVESTIGATOR comment)"
|
|
134
|
+
BACKFILL_STRIP_ONLY=$((BACKFILL_STRIP_ONLY + 1))
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
else
|
|
138
|
+
# No INVESTIGATOR comment — strip-only
|
|
139
|
+
gh issue edit $NUM {GH_FLAG} --remove-label "needs-validation" 2>/dev/null || true
|
|
140
|
+
echo "#$NUM: needs-validation stripped (no FORGE:INVESTIGATOR comment found)"
|
|
141
|
+
BACKFILL_STRIP_ONLY=$((BACKFILL_STRIP_ONLY + 1))
|
|
142
|
+
fi
|
|
98
143
|
done
|
|
144
|
+
|
|
145
|
+
echo "Backfill complete: validated=$BACKFILL_VALIDATED false-positive=$BACKFILL_FALSE_POSITIVE strip-only=$BACKFILL_STRIP_ONLY"
|
|
99
146
|
```
|
|
100
147
|
|
|
101
148
|
### 1C: Report closed issues with NO workflow label
|
|
@@ -465,7 +512,9 @@ fi
|
|
|
465
512
|
| workflow:in-review → workflow:merged | {N} |
|
|
466
513
|
| workflow:building → workflow:merged | {N} |
|
|
467
514
|
| workflow:investigating → cleaned | {N} |
|
|
468
|
-
| needs-validation
|
|
515
|
+
| needs-validation → validated (backfill H1) | {N} |
|
|
516
|
+
| needs-validation → false-positive (backfill H1) | {N} |
|
|
517
|
+
| needs-validation stripped (no verdict) | {N} |
|
|
469
518
|
|
|
470
519
|
### Stale-Open Intermediate Issues (Phase 1D)
|
|
471
520
|
| Issue | Label | Last Update | Action |
|
package/commands/optimize.md
CHANGED
|
@@ -401,6 +401,589 @@ Run /work-on on a few issues to let the pipeline capture corrections, then re-ru
|
|
|
401
401
|
|
|
402
402
|
---
|
|
403
403
|
|
|
404
|
+
## Phase 5: Promotion Loop (Tier-A and Tier-B)
|
|
405
|
+
|
|
406
|
+
<!-- Added: forge#1739 -->
|
|
407
|
+
|
|
408
|
+
**Purpose**: Scan the Forge Ledger for validated finding patterns that have recurred ≥3 times and promote them — either as a Tier-A mechanizable check script in `gate.d/` or as a Tier-B prose entry in `devdocs/learned-rules/{domain}.md`. Both promotion paths open as a PR for human review. Checks are never silently auto-enforced.
|
|
409
|
+
|
|
410
|
+
**Skip if**: `--dry-run` flag is set (print what would be promoted without creating PRs).
|
|
411
|
+
|
|
412
|
+
**Skip if**: The Forge Ledger CLI (`forge recall`) is not available — log a note and continue.
|
|
413
|
+
|
|
414
|
+
### 5A: Query the Ledger for promotion candidates
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# forge recall requires the Forge Ledger from #1732 to be indexed.
|
|
418
|
+
# If not available, skip Phase 5 with a note.
|
|
419
|
+
if ! command -v forge >/dev/null 2>&1; then
|
|
420
|
+
echo "Phase 5: forge CLI not found — skipping promotion loop."
|
|
421
|
+
echo " Install: https://github.com/RapierCraftStudios/forgedock#forge-ledger"
|
|
422
|
+
return 0 2>/dev/null || exit 0
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Query for patterns with ≥3 validated occurrences (never unvalidated or false-positive)
|
|
426
|
+
# Output: one JSON object per line with fields: slug, domain, count, type, summary, citations[]
|
|
427
|
+
PROMOTION_CANDIDATES=$(forge recall \
|
|
428
|
+
--filter validated \
|
|
429
|
+
--min-count 3 \
|
|
430
|
+
--output jsonl \
|
|
431
|
+
--repo "$GH_REPO" \
|
|
432
|
+
2>/dev/null || echo '')
|
|
433
|
+
|
|
434
|
+
if [ -z "$PROMOTION_CANDIDATES" ]; then
|
|
435
|
+
echo "Phase 5: No patterns with ≥3 validated occurrences found — nothing to promote."
|
|
436
|
+
return 0 2>/dev/null || exit 0
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
CANDIDATE_COUNT=$(echo "$PROMOTION_CANDIDATES" | grep -c '"slug"' 2>/dev/null || echo 0)
|
|
440
|
+
echo "Phase 5: Found $CANDIDATE_COUNT promotion candidate(s)"
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 5B: Classify each candidate as Tier-A or Tier-B
|
|
444
|
+
|
|
445
|
+
For each candidate pattern, classify it:
|
|
446
|
+
|
|
447
|
+
| Tier | Condition | Promotion target |
|
|
448
|
+
|------|-----------|-----------------|
|
|
449
|
+
| **Tier-A** | Pattern is mechanically grep-detectable in a diff (literal string, regex, or structural property) AND can be expressed as a bash check returning the standard `SLUG \| SEVERITY \| FILE \| MESSAGE` format | Draft gate.d script → PR |
|
|
450
|
+
| **Tier-B** | Pattern requires LLM reasoning to detect (code semantics, logic flow, cross-file reasoning) OR the fix depends on project-specific context that can't be encoded in a grep | Draft learned-rules.md entry → PR |
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
TIER_A_PROMOTIONS=()
|
|
454
|
+
TIER_B_PROMOTIONS=()
|
|
455
|
+
|
|
456
|
+
while IFS= read -r candidate; do
|
|
457
|
+
[ -z "$candidate" ] && continue
|
|
458
|
+
SLUG=$(echo "$candidate" | jq -r '.slug // ""' 2>/dev/null)
|
|
459
|
+
DOMAIN=$(echo "$candidate" | jq -r '.domain // "GENERAL"' 2>/dev/null)
|
|
460
|
+
COUNT=$(echo "$candidate" | jq -r '.count // 0' 2>/dev/null)
|
|
461
|
+
TYPE=$(echo "$candidate" | jq -r '.type // "prose"' 2>/dev/null) # "mechanical" or "prose"
|
|
462
|
+
SUMMARY=$(echo "$candidate" | jq -r '.summary // ""' 2>/dev/null)
|
|
463
|
+
CITATIONS=$(echo "$candidate" | jq -r '[.citations[] | "#" + tostring] | join(", ")' 2>/dev/null)
|
|
464
|
+
|
|
465
|
+
[ -z "$SLUG" ] && continue
|
|
466
|
+
|
|
467
|
+
if [ "$TYPE" = "mechanical" ]; then
|
|
468
|
+
TIER_A_PROMOTIONS+=("$candidate")
|
|
469
|
+
echo " Tier-A candidate: $SLUG (domain=$DOMAIN, count=$COUNT, citations=$CITATIONS)"
|
|
470
|
+
else
|
|
471
|
+
TIER_B_PROMOTIONS+=("$candidate")
|
|
472
|
+
echo " Tier-B candidate: $SLUG (domain=$DOMAIN, count=$COUNT, citations=$CITATIONS)"
|
|
473
|
+
fi
|
|
474
|
+
done <<< "$PROMOTION_CANDIDATES"
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 5C: Draft Tier-A gate.d scripts
|
|
478
|
+
|
|
479
|
+
For each Tier-A candidate, draft a gate.d script and open it as a PR:
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
GATE_D_DIR="$SCRIPTS_DIR/gate.d"
|
|
483
|
+
mkdir -p "$GATE_D_DIR"
|
|
484
|
+
|
|
485
|
+
for candidate in "${TIER_A_PROMOTIONS[@]}"; do
|
|
486
|
+
SLUG=$(echo "$candidate" | jq -r '.slug' 2>/dev/null)
|
|
487
|
+
DOMAIN=$(echo "$candidate" | jq -r '.domain // "GENERAL"' 2>/dev/null)
|
|
488
|
+
SUMMARY=$(echo "$candidate" | jq -r '.summary' 2>/dev/null)
|
|
489
|
+
PATTERN=$(echo "$candidate" | jq -r '.pattern // ""' 2>/dev/null) # grep-ready pattern string
|
|
490
|
+
CITATIONS=$(echo "$candidate" | jq -r '[.citations[] | "#" + tostring] | join(", ")' 2>/dev/null)
|
|
491
|
+
COUNT=$(echo "$candidate" | jq -r '.count' 2>/dev/null)
|
|
492
|
+
SEVERITY=$(echo "$candidate" | jq -r '.severity // "HIGH"' 2>/dev/null)
|
|
493
|
+
|
|
494
|
+
SCRIPT_PATH="$GATE_D_DIR/${SLUG}.sh"
|
|
495
|
+
|
|
496
|
+
# Skip if already promoted (idempotent)
|
|
497
|
+
if [ -f "$SCRIPT_PATH" ] && [ "$DRY_RUN" != "true" ]; then
|
|
498
|
+
echo " Tier-A: $SLUG — already exists at $SCRIPT_PATH, skipping"
|
|
499
|
+
continue
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
503
|
+
echo " [DRY RUN] Would create gate.d/${SLUG}.sh (domain=$DOMAIN, citations=$CITATIONS)"
|
|
504
|
+
continue
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# Generate the gate.d script
|
|
508
|
+
cat > "$SCRIPT_PATH" << GATE_SCRIPT_EOF
|
|
509
|
+
#!/usr/bin/env bash
|
|
510
|
+
# gate.d/${SLUG}.sh — auto-generated by /optimize Phase 5
|
|
511
|
+
# Domain: ${DOMAIN}
|
|
512
|
+
# Pattern: ${SUMMARY}
|
|
513
|
+
# Evidence: ${COUNT} validated occurrences — ${CITATIONS}
|
|
514
|
+
# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
515
|
+
#
|
|
516
|
+
# CONTRACT:
|
|
517
|
+
# \$1 = absolute path to unified diff (git diff HEAD)
|
|
518
|
+
# \$2 = absolute path to worktree root
|
|
519
|
+
# stdout = findings lines: SLUG | SEVERITY | FILE | MESSAGE
|
|
520
|
+
# exit 0 = pass, exit 1 = findings, exit 2 = inapplicable
|
|
521
|
+
# 30s timeout enforced by quality-gate (fail-closed)
|
|
522
|
+
#
|
|
523
|
+
# EDIT THIS SCRIPT: the pattern below was auto-generated. Review and test
|
|
524
|
+
# it against real diffs before the PR is merged.
|
|
525
|
+
|
|
526
|
+
set -euo pipefail
|
|
527
|
+
|
|
528
|
+
DIFF_PATH="\${1:-}"
|
|
529
|
+
WORKTREE="\${2:-}"
|
|
530
|
+
FOUND=0
|
|
531
|
+
|
|
532
|
+
if [ -z "\$DIFF_PATH" ] || [ ! -f "\$DIFF_PATH" ]; then
|
|
533
|
+
exit 2 # inapplicable — no diff provided
|
|
534
|
+
fi
|
|
535
|
+
|
|
536
|
+
# Check for the pattern in added lines of the diff
|
|
537
|
+
# TODO: refine this pattern based on the specific defect class
|
|
538
|
+
PATTERN='${PATTERN}'
|
|
539
|
+
if [ -z "\$PATTERN" ]; then
|
|
540
|
+
exit 2 # no pattern configured — mark inapplicable until author fills it in
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
while IFS= read -r line; do
|
|
544
|
+
[ -z "\$line" ] && continue
|
|
545
|
+
FILE=\$(echo "\$line" | grep -oP '(?<=\+\+\+ b/).*' || true)
|
|
546
|
+
done < <(grep '^+++ b/' "\$DIFF_PATH" 2>/dev/null)
|
|
547
|
+
|
|
548
|
+
# Scan added lines for the pattern
|
|
549
|
+
MATCHES=\$(grep -nP "^\+" "\$DIFF_PATH" 2>/dev/null | grep -v "^+++" | grep -P "\$PATTERN" 2>/dev/null || true)
|
|
550
|
+
|
|
551
|
+
if [ -n "\$MATCHES" ]; then
|
|
552
|
+
while IFS= read -r match; do
|
|
553
|
+
[ -z "\$match" ] && continue
|
|
554
|
+
echo "GATD-${SLUG} | ${SEVERITY} | (diff) | ${SUMMARY} — pattern matched in added lines. Citations: ${CITATIONS}"
|
|
555
|
+
FOUND=1
|
|
556
|
+
done <<< "\$MATCHES"
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
exit \$FOUND
|
|
560
|
+
GATE_SCRIPT_EOF
|
|
561
|
+
chmod +x "$SCRIPT_PATH"
|
|
562
|
+
echo " Tier-A: Created gate.d/${SLUG}.sh (citations=$CITATIONS)"
|
|
563
|
+
done
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
After drafting all Tier-A scripts, open a single PR with all new gate.d scripts:
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
if [ "${#TIER_A_PROMOTIONS[@]}" -gt 0 ] && [ "$DRY_RUN" != "true" ]; then
|
|
570
|
+
PROMO_BRANCH="feat/gate-d-promotion-$(date -u +%Y%m%d)"
|
|
571
|
+
git -C "$REPO_PATH" checkout -b "$PROMO_BRANCH" "origin/${STAGING_BRANCH:-staging}" 2>/dev/null || \
|
|
572
|
+
git -C "$REPO_PATH" checkout "$PROMO_BRANCH" 2>/dev/null
|
|
573
|
+
git -C "$REPO_PATH" add "$GATE_D_DIR"
|
|
574
|
+
git -C "$REPO_PATH" diff --cached --quiet || \
|
|
575
|
+
git -C "$REPO_PATH" commit -s -m "feat(gate.d): promote ${#TIER_A_PROMOTIONS[@]} learned check(s) from Ledger patterns"
|
|
576
|
+
git -C "$REPO_PATH" push -u origin "$PROMO_BRANCH"
|
|
577
|
+
|
|
578
|
+
CITATIONS_BODY=""
|
|
579
|
+
for candidate in "${TIER_A_PROMOTIONS[@]}"; do
|
|
580
|
+
SLUG=$(echo "$candidate" | jq -r '.slug' 2>/dev/null)
|
|
581
|
+
SUMMARY=$(echo "$candidate" | jq -r '.summary' 2>/dev/null)
|
|
582
|
+
COUNT=$(echo "$candidate" | jq -r '.count' 2>/dev/null)
|
|
583
|
+
CITS=$(echo "$candidate" | jq -r '[.citations[] | "#" + tostring] | join(", ")' 2>/dev/null)
|
|
584
|
+
CITATIONS_BODY="${CITATIONS_BODY}
|
|
585
|
+
- **\`${SLUG}\`**: ${SUMMARY} — ${COUNT} validated occurrences (${CITS})"
|
|
586
|
+
done
|
|
587
|
+
|
|
588
|
+
gh pr create -R "$GH_REPO" \
|
|
589
|
+
--base "${STAGING_BRANCH:-staging}" \
|
|
590
|
+
--head "$PROMO_BRANCH" \
|
|
591
|
+
--title "feat(gate.d): promote ${#TIER_A_PROMOTIONS[@]} learned check(s) from recurring patterns" \
|
|
592
|
+
--body "$(cat <<PR_BODY_EOF
|
|
593
|
+
## Summary
|
|
594
|
+
|
|
595
|
+
This PR was generated by \`/optimize\` Phase 5. It promotes ${#TIER_A_PROMOTIONS[@]} recurring pattern(s) from the Forge Ledger into deterministic gate.d check scripts.
|
|
596
|
+
|
|
597
|
+
Each promoted script represents a defect class that appeared ≥3 times in validated findings for this codebase. Once merged, the check runs pre-commit on every future PR.
|
|
598
|
+
|
|
599
|
+
## Promoted checks
|
|
600
|
+
${CITATIONS_BODY}
|
|
601
|
+
|
|
602
|
+
## Review notes
|
|
603
|
+
|
|
604
|
+
**Please review each gate.d script carefully before merging.**
|
|
605
|
+
|
|
606
|
+
- Verify the grep pattern matches the intended defect class
|
|
607
|
+
- Test against a real diff that should fire (and one that should not)
|
|
608
|
+
- The pattern comment in each script links to the source finding issues
|
|
609
|
+
- Edit the \`PATTERN\` variable in the script if the auto-generated pattern needs refinement
|
|
610
|
+
- If a promoted check is incorrect, close this PR and file a rule-contest issue
|
|
611
|
+
|
|
612
|
+
## gate.d contract
|
|
613
|
+
|
|
614
|
+
Each script receives:
|
|
615
|
+
- \$1 = path to unified diff
|
|
616
|
+
- \$2 = path to worktree root
|
|
617
|
+
- exits 0 (pass), 1 (findings on stdout), or 2 (inapplicable)
|
|
618
|
+
- 30-second timeout enforced by quality-gate (fail-closed on timeout)
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
Generated by: \`/optimize\` Phase 5 — pattern promotion loop
|
|
622
|
+
PR_BODY_EOF
|
|
623
|
+
)"
|
|
624
|
+
echo "Phase 5: Opened Tier-A promotion PR on branch $PROMO_BRANCH"
|
|
625
|
+
fi
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 5D: Draft Tier-B learned-rules.md entries
|
|
629
|
+
|
|
630
|
+
For each Tier-B candidate, append a prose entry to `devdocs/learned-rules/{domain}.md` and open a PR:
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
LEARNED_RULES_DIR="$REPO_PATH/devdocs/learned-rules"
|
|
634
|
+
mkdir -p "$LEARNED_RULES_DIR"
|
|
635
|
+
|
|
636
|
+
TIER_B_ENTRIES=""
|
|
637
|
+
for candidate in "${TIER_B_PROMOTIONS[@]}"; do
|
|
638
|
+
SLUG=$(echo "$candidate" | jq -r '.slug' 2>/dev/null)
|
|
639
|
+
DOMAIN=$(echo "$candidate" | jq -r '.domain // "GENERAL"' 2>/dev/null)
|
|
640
|
+
SUMMARY=$(echo "$candidate" | jq -r '.summary' 2>/dev/null)
|
|
641
|
+
COUNT=$(echo "$candidate" | jq -r '.count' 2>/dev/null)
|
|
642
|
+
CITATIONS=$(echo "$candidate" | jq -r '[.citations[] | "#" + tostring] | join(", ")' 2>/dev/null)
|
|
643
|
+
SEVERITY=$(echo "$candidate" | jq -r '.severity // "MEDIUM"' 2>/dev/null)
|
|
644
|
+
RULES_FILE="$LEARNED_RULES_DIR/${DOMAIN}.md"
|
|
645
|
+
|
|
646
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
647
|
+
echo " [DRY RUN] Would append to devdocs/learned-rules/${DOMAIN}.md — rule: $SLUG"
|
|
648
|
+
continue
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
# Create file with header if it doesn't exist
|
|
652
|
+
if [ ! -f "$RULES_FILE" ]; then
|
|
653
|
+
cat > "$RULES_FILE" << RULES_HEADER_EOF
|
|
654
|
+
# Learned Rules — ${DOMAIN} Domain
|
|
655
|
+
|
|
656
|
+
This file contains Tier-B prose rules promoted by \`/optimize\` from recurring validated findings.
|
|
657
|
+
These rules are injected into the quality gate's ${DOMAIN} domain scan to guide LLM-assisted checks.
|
|
658
|
+
|
|
659
|
+
Each rule carries citations to the source findings that justified its promotion.
|
|
660
|
+
Rules are removed when evidence is refuted (two sustained contests → promotion issue reopened).
|
|
661
|
+
|
|
662
|
+
<!-- FORGE:LEARNED_RULES domain=${DOMAIN} -->
|
|
663
|
+
RULES_HEADER_EOF
|
|
664
|
+
echo " Tier-B: Created $RULES_FILE"
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
# Check if rule already exists (idempotent by slug)
|
|
668
|
+
if grep -q "<!-- rule:${SLUG} -->" "$RULES_FILE" 2>/dev/null; then
|
|
669
|
+
echo " Tier-B: $SLUG already present in ${DOMAIN}.md — skipping"
|
|
670
|
+
continue
|
|
671
|
+
fi
|
|
672
|
+
|
|
673
|
+
# Append the rule entry
|
|
674
|
+
cat >> "$RULES_FILE" << RULE_EOF
|
|
675
|
+
|
|
676
|
+
## ${SLUG} <!-- rule:${SLUG} -->
|
|
677
|
+
|
|
678
|
+
**Severity**: ${SEVERITY}
|
|
679
|
+
**Pattern type**: Tier-B (prose — requires reasoning to detect)
|
|
680
|
+
**Validated occurrences**: ${COUNT}
|
|
681
|
+
**Citations**: ${CITATIONS}
|
|
682
|
+
**Promoted**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
683
|
+
|
|
684
|
+
${SUMMARY}
|
|
685
|
+
|
|
686
|
+
**What to check**: When reviewing ${DOMAIN} domain changes, flag any instance matching this pattern as \`SHELL-LEARNED-${SLUG} | ${SEVERITY} | {file} | {description}\`.
|
|
687
|
+
|
|
688
|
+
**Refutation**: If this rule produces a false finding, file a rule-contest issue with the blocked diff attached. One sustained contest → rule demoted to advisory. Two sustained contests → this rule entry is removed and the promotion issue is reopened.
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
RULE_EOF
|
|
692
|
+
TIER_B_ENTRIES="${TIER_B_ENTRIES} devdocs/learned-rules/${DOMAIN}.md"
|
|
693
|
+
echo " Tier-B: Appended rule $SLUG to devdocs/learned-rules/${DOMAIN}.md (citations=$CITATIONS)"
|
|
694
|
+
done
|
|
695
|
+
|
|
696
|
+
# Open PR for Tier-B entries
|
|
697
|
+
if [ -n "$TIER_B_ENTRIES" ] && [ "$DRY_RUN" != "true" ]; then
|
|
698
|
+
TIER_B_BRANCH="feat/learned-rules-promotion-$(date -u +%Y%m%d)"
|
|
699
|
+
git -C "$REPO_PATH" checkout -b "$TIER_B_BRANCH" "origin/${STAGING_BRANCH:-staging}" 2>/dev/null || \
|
|
700
|
+
git -C "$REPO_PATH" checkout "$TIER_B_BRANCH" 2>/dev/null
|
|
701
|
+
# Stage only learned-rules files changed in this run
|
|
702
|
+
for f in $TIER_B_ENTRIES; do
|
|
703
|
+
git -C "$REPO_PATH" add "$f" 2>/dev/null || true
|
|
704
|
+
done
|
|
705
|
+
git -C "$REPO_PATH" diff --cached --quiet || \
|
|
706
|
+
git -C "$REPO_PATH" commit -s -m "feat(learned-rules): promote ${#TIER_B_PROMOTIONS[@]} Tier-B prose rule(s) from Ledger patterns"
|
|
707
|
+
git -C "$REPO_PATH" push -u origin "$TIER_B_BRANCH"
|
|
708
|
+
|
|
709
|
+
gh pr create -R "$GH_REPO" \
|
|
710
|
+
--base "${STAGING_BRANCH:-staging}" \
|
|
711
|
+
--head "$TIER_B_BRANCH" \
|
|
712
|
+
--title "feat(learned-rules): promote ${#TIER_B_PROMOTIONS[@]} Tier-B prose rule(s) from recurring patterns" \
|
|
713
|
+
--body "$(cat <<TIER_B_PR_EOF
|
|
714
|
+
## Summary
|
|
715
|
+
|
|
716
|
+
This PR was generated by \`/optimize\` Phase 5. It adds ${#TIER_B_PROMOTIONS[@]} prose rule(s) to \`devdocs/learned-rules/\` from recurring Ledger patterns.
|
|
717
|
+
|
|
718
|
+
These are **Tier-B** rules — defect classes that cannot be caught by a mechanical grep but recur in LLM-assisted gate scans. Once merged, the relevant domain scan in \`/quality-gate\` will inject these rules as additional context (~200 tokens per domain).
|
|
719
|
+
|
|
720
|
+
## Review notes
|
|
721
|
+
|
|
722
|
+
- Verify each rule accurately describes the defect class and doesn't over-generalize
|
|
723
|
+
- Rules carry evidence citations — check that the cited findings actually justify the rule
|
|
724
|
+
- A builder can contest a rule by filing a rule-contest issue with the blocked diff
|
|
725
|
+
- Rules are injected as Tier-B guidance, not as mechanical blockers
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
Generated by: \`/optimize\` Phase 5 — pattern promotion loop
|
|
729
|
+
TIER_B_PR_EOF
|
|
730
|
+
)"
|
|
731
|
+
echo "Phase 5: Opened Tier-B learned-rules promotion PR on branch $TIER_B_BRANCH"
|
|
732
|
+
fi
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### 5E: Phase 5 summary
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
echo ""
|
|
739
|
+
echo "--- Phase 5: Promotion Loop Summary ---"
|
|
740
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
741
|
+
echo " Mode: DRY RUN — no PRs opened, no files written"
|
|
742
|
+
echo " Tier-A candidates: ${#TIER_A_PROMOTIONS[@]}"
|
|
743
|
+
echo " Tier-B candidates: ${#TIER_B_PROMOTIONS[@]}"
|
|
744
|
+
else
|
|
745
|
+
echo " Tier-A gate.d scripts promoted: ${#TIER_A_PROMOTIONS[@]}"
|
|
746
|
+
echo " Tier-B learned-rules entries promoted: ${#TIER_B_PROMOTIONS[@]}"
|
|
747
|
+
echo ""
|
|
748
|
+
echo " Each promotion lands as a PR for human review before enforcement."
|
|
749
|
+
echo " Demotion is automatic on evidence; promotion requires a human."
|
|
750
|
+
fi
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## Phase 6: Contribution Path (Outbound Exchange)
|
|
756
|
+
|
|
757
|
+
<!-- Added: forge#1746 -->
|
|
758
|
+
|
|
759
|
+
**Purpose**: When a promoted learned rule (from Phase 5) generalizes beyond this repo — no repo paths, no project-specific identifiers in the card body — offer to contribute it to a subscribed exchange repo so other ForgeDock installations can benefit. This is the outbound half of the pattern exchange loop.
|
|
760
|
+
|
|
761
|
+
**CRITICAL: Never auto-publish.** All outbound actions require explicit human approval. The contribution path opens a PR DRAFT and stops — a human must review and approve before anything leaves this repo's private context.
|
|
762
|
+
|
|
763
|
+
**Skip if**: `--dry-run` flag is set.
|
|
764
|
+
**Skip if**: No exchange repo is configured in `forge.yaml → pattern_feeds.feeds` (check `contributing_to` field or any feed with `slug` marked as canonical exchange target).
|
|
765
|
+
**Skip if**: No promoted patterns exist from Phase 5 (nothing to contribute).
|
|
766
|
+
|
|
767
|
+
### 6A: Identify generalizable patterns
|
|
768
|
+
|
|
769
|
+
Scan Phase 5 promotion output for candidates that pass ALL three generalization tests:
|
|
770
|
+
|
|
771
|
+
**Test 1 — No repo paths**: Card body must not contain absolute paths, repo-specific directories, or project identifiers.
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
# Extract promoted card bodies from Phase 5 TIER_A_PROMOTIONS and TIER_B_PROMOTIONS
|
|
775
|
+
GENERALIZABLE_CANDIDATES=()
|
|
776
|
+
|
|
777
|
+
for SLUG in "${TIER_A_PROMOTIONS[@]}" "${TIER_B_PROMOTIONS[@]}"; do
|
|
778
|
+
# Read card body from gate.d/ or devdocs/learned-rules/ (where Phase 5 wrote it)
|
|
779
|
+
CARD_BODY=""
|
|
780
|
+
if [ -f "${WORKTREE_PATH}/.forgedock/gate.d/${SLUG}.sh" ]; then
|
|
781
|
+
CARD_BODY=$(cat "${WORKTREE_PATH}/.forgedock/gate.d/${SLUG}.sh" 2>/dev/null)
|
|
782
|
+
elif [ -f "${WORKTREE_PATH}/devdocs/learned-rules/${SLUG}.md" ]; then
|
|
783
|
+
CARD_BODY=$(cat "${WORKTREE_PATH}/devdocs/learned-rules/${SLUG}.md" 2>/dev/null)
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
[ -z "$CARD_BODY" ] && continue
|
|
787
|
+
|
|
788
|
+
# Test 1: no absolute paths (e.g. /home/, /app/, /srv/, /opt/)
|
|
789
|
+
if echo "$CARD_BODY" | grep -qE '^[[:space:]]*(path|dir|file).*\/home\/|\/app\/|\/srv\/|\/opt\/'; then
|
|
790
|
+
echo "6A: SKIP $SLUG — contains absolute path references (not generalizable)"
|
|
791
|
+
continue
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
# Test 2: no project-specific identifiers
|
|
795
|
+
PROJECT_NAME=$(yq '.project.name // ""' "$FORGE_YAML" 2>/dev/null || echo '')
|
|
796
|
+
PROJECT_REPO=$(yq '.project.repo // ""' "$FORGE_YAML" 2>/dev/null || echo '')
|
|
797
|
+
if [ -n "$PROJECT_NAME" ] && echo "$CARD_BODY" | grep -qi "$PROJECT_NAME"; then
|
|
798
|
+
echo "6A: SKIP $SLUG — contains project name '$PROJECT_NAME' (not generalizable)"
|
|
799
|
+
continue
|
|
800
|
+
fi
|
|
801
|
+
if [ -n "$PROJECT_REPO" ] && echo "$CARD_BODY" | grep -qi "$PROJECT_REPO"; then
|
|
802
|
+
echo "6A: SKIP $SLUG — contains repo name '$PROJECT_REPO' (not generalizable)"
|
|
803
|
+
continue
|
|
804
|
+
fi
|
|
805
|
+
|
|
806
|
+
# Test 3: has stack tags (required for exchange repo schema)
|
|
807
|
+
# (stack tags are set during Phase 5 promotion from ledger metadata)
|
|
808
|
+
STACK_TAGS=$(forge recall --slug "$SLUG" --field stacks 2>/dev/null || echo '')
|
|
809
|
+
if [ -z "$STACK_TAGS" ]; then
|
|
810
|
+
echo "6A: SKIP $SLUG — no stack tags found (required for exchange schema)"
|
|
811
|
+
continue
|
|
812
|
+
fi
|
|
813
|
+
|
|
814
|
+
echo "6A: CANDIDATE $SLUG — passes generalization tests"
|
|
815
|
+
GENERALIZABLE_CANDIDATES+=("$SLUG")
|
|
816
|
+
done
|
|
817
|
+
|
|
818
|
+
if [ "${#GENERALIZABLE_CANDIDATES[@]}" -eq 0 ]; then
|
|
819
|
+
echo "Phase 6: No generalizable candidates found — skipping contribution path."
|
|
820
|
+
return 0 2>/dev/null || exit 0
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
echo "Phase 6: ${#GENERALIZABLE_CANDIDATES[@]} generalizable candidate(s): ${GENERALIZABLE_CANDIDATES[*]}"
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### 6B: Resolve exchange target repo
|
|
827
|
+
|
|
828
|
+
Read the first configured feed as the contribution target. If no feed is configured, skip.
|
|
829
|
+
|
|
830
|
+
```bash
|
|
831
|
+
EXCHANGE_REPO=$(yq '.pattern_feeds.feeds[0].repo // ""' "$FORGE_YAML" 2>/dev/null || echo '')
|
|
832
|
+
EXCHANGE_PATH=$(yq '.pattern_feeds.feeds[0].path // "cards"' "$FORGE_YAML" 2>/dev/null || echo 'cards')
|
|
833
|
+
|
|
834
|
+
if [ -z "$EXCHANGE_REPO" ]; then
|
|
835
|
+
echo "Phase 6: No exchange repo configured in forge.yaml → pattern_feeds.feeds — skipping."
|
|
836
|
+
return 0 2>/dev/null || exit 0
|
|
837
|
+
fi
|
|
838
|
+
|
|
839
|
+
# Verify the exchange repo is accessible
|
|
840
|
+
if ! gh repo view "$EXCHANGE_REPO" >/dev/null 2>&1; then
|
|
841
|
+
echo "Phase 6: Exchange repo $EXCHANGE_REPO is not accessible — skipping."
|
|
842
|
+
return 0 2>/dev/null || exit 0
|
|
843
|
+
fi
|
|
844
|
+
|
|
845
|
+
echo "Phase 6: Contribution target → $EXCHANGE_REPO ($EXCHANGE_PATH/)"
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### 6C: Draft contribution PR (HUMAN APPROVAL REQUIRED)
|
|
849
|
+
|
|
850
|
+
For each generalizable candidate, build a card file following the exchange repo schema (`{stack}/{pattern-slug}.md`) and open a **DRAFT PR** on the exchange repo. The pipeline STOPS here — a human must review the card and approve the PR before anything is merged or published.
|
|
851
|
+
|
|
852
|
+
**Human approval gate**: This step opens a DRAFT PR and posts a GitHub comment on the current issue with the link. No auto-merge. No CI trigger that merges automatically. The draft status ensures the exchange repo maintainer (a human) reviews and approves the outbound content before it becomes public knowledge.
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
for SLUG in "${GENERALIZABLE_CANDIDATES[@]}"; do
|
|
856
|
+
# Build exchange card from ledger metadata
|
|
857
|
+
STACK_TAGS=$(forge recall --slug "$SLUG" --field stacks 2>/dev/null || echo 'generic')
|
|
858
|
+
PREVENTION_RULE=$(forge recall --slug "$SLUG" --field prevention 2>/dev/null || echo '')
|
|
859
|
+
ROOT_CAUSE_SHAPE=$(forge recall --slug "$SLUG" --field root_cause_shape 2>/dev/null || echo '')
|
|
860
|
+
SUMMARY=$(forge recall --slug "$SLUG" --field summary 2>/dev/null || echo '')
|
|
861
|
+
GATE_TEMPLATE=$(forge recall --slug "$SLUG" --field gate_template 2>/dev/null || echo '')
|
|
862
|
+
|
|
863
|
+
# Determine card path: use first stack tag as directory
|
|
864
|
+
PRIMARY_STACK=$(echo "$STACK_TAGS" | tr ',' '\n' | head -1 | tr -d ' "[]')
|
|
865
|
+
CARD_PATH="${EXCHANGE_PATH}/${PRIMARY_STACK}/${SLUG}.md"
|
|
866
|
+
|
|
867
|
+
# Build card body following exchange repo schema
|
|
868
|
+
CARD_BODY="# Pattern: ${SLUG}
|
|
869
|
+
|
|
870
|
+
## Stacks
|
|
871
|
+
${STACK_TAGS}
|
|
872
|
+
|
|
873
|
+
## Root Cause Shape
|
|
874
|
+
${ROOT_CAUSE_SHAPE:-Unknown — see prevention rule below.}
|
|
875
|
+
|
|
876
|
+
## Prevention Rule
|
|
877
|
+
${PREVENTION_RULE:-No prevention rule captured.}
|
|
878
|
+
|
|
879
|
+
## Summary
|
|
880
|
+
${SUMMARY}
|
|
881
|
+
"
|
|
882
|
+
|
|
883
|
+
# Append gate.d check template if present (optional field)
|
|
884
|
+
if [ -n "$GATE_TEMPLATE" ]; then
|
|
885
|
+
CARD_BODY="${CARD_BODY}
|
|
886
|
+
## gate.d Check Template
|
|
887
|
+
|
|
888
|
+
\`\`\`bash
|
|
889
|
+
${GATE_TEMPLATE}
|
|
890
|
+
\`\`\`
|
|
891
|
+
"
|
|
892
|
+
fi
|
|
893
|
+
|
|
894
|
+
# NOTE: Only run this block if $DRY_RUN is false
|
|
895
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
896
|
+
echo "DRY RUN: Would draft contribution PR for $SLUG → $EXCHANGE_REPO:$CARD_PATH"
|
|
897
|
+
continue
|
|
898
|
+
fi
|
|
899
|
+
|
|
900
|
+
# Create contribution branch and draft PR on the exchange repo
|
|
901
|
+
CONTRIB_BRANCH="contrib/${SLUG}-$(date +%Y%m%d)"
|
|
902
|
+
|
|
903
|
+
# Write card to a temp file for gh pr creation (requires a clone of the exchange repo)
|
|
904
|
+
# NOTE: This requires the exchange repo to be cloned locally. If it is not, skip with a note.
|
|
905
|
+
EXCHANGE_LOCAL=$(yq '.pattern_feeds.feeds[0].local_path // ""' "$FORGE_YAML" 2>/dev/null || echo '')
|
|
906
|
+
|
|
907
|
+
if [ -z "$EXCHANGE_LOCAL" ] || [ ! -d "$EXCHANGE_LOCAL" ]; then
|
|
908
|
+
echo "Phase 6: SKIP $SLUG — exchange repo not cloned locally."
|
|
909
|
+
echo " To contribute: clone $EXCHANGE_REPO, set pattern_feeds.feeds[0].local_path in forge.yaml, re-run /optimize."
|
|
910
|
+
continue
|
|
911
|
+
fi
|
|
912
|
+
|
|
913
|
+
# Create branch in exchange repo
|
|
914
|
+
git -C "$EXCHANGE_LOCAL" fetch origin 2>/dev/null
|
|
915
|
+
git -C "$EXCHANGE_LOCAL" checkout -b "$CONTRIB_BRANCH" origin/main 2>/dev/null || {
|
|
916
|
+
echo "Phase 6: SKIP $SLUG — could not create branch $CONTRIB_BRANCH in $EXCHANGE_LOCAL"
|
|
917
|
+
continue
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
# Write card file
|
|
921
|
+
mkdir -p "$EXCHANGE_LOCAL/${EXCHANGE_PATH}/${PRIMARY_STACK}"
|
|
922
|
+
echo "$CARD_BODY" > "$EXCHANGE_LOCAL/${EXCHANGE_PATH}/${PRIMARY_STACK}/${SLUG}.md"
|
|
923
|
+
git -C "$EXCHANGE_LOCAL" add "${EXCHANGE_PATH}/${PRIMARY_STACK}/${SLUG}.md"
|
|
924
|
+
git -C "$EXCHANGE_LOCAL" commit -s -m "feat(cards): add ${SLUG} pattern (${PRIMARY_STACK})" 2>/dev/null
|
|
925
|
+
|
|
926
|
+
# Push and open DRAFT PR — human approval required before merge
|
|
927
|
+
git -C "$EXCHANGE_LOCAL" push origin "$CONTRIB_BRANCH" 2>/dev/null && \
|
|
928
|
+
DRAFT_PR_URL=$(gh pr create \
|
|
929
|
+
-R "$EXCHANGE_REPO" \
|
|
930
|
+
--base main \
|
|
931
|
+
--head "$CONTRIB_BRANCH" \
|
|
932
|
+
--draft \
|
|
933
|
+
--title "feat(cards): contribute ${SLUG} pattern" \
|
|
934
|
+
--body "$(cat <<CONTRIB_PR_EOF
|
|
935
|
+
## Pattern Contribution: \`${SLUG}\`
|
|
936
|
+
|
|
937
|
+
This is an **automated draft** generated by \`/optimize\` Phase 6 on $(date -u +%Y-%m-%d).
|
|
938
|
+
|
|
939
|
+
**HUMAN REVIEW REQUIRED before this PR is merged or published.**
|
|
940
|
+
|
|
941
|
+
The card was automatically identified as generalizable (no repo paths, no project identifiers, has stack tags). However, automatic generalization detection is heuristic — please verify:
|
|
942
|
+
|
|
943
|
+
- [ ] Card body contains no proprietary information, internal system names, or project-specific paths
|
|
944
|
+
- [ ] Prevention rule is accurate and actionable for other teams using the same stack
|
|
945
|
+
- [ ] Stack tags (\`${STACK_TAGS}\`) are correct
|
|
946
|
+
- [ ] Root cause shape describes the bug class, not a specific incident
|
|
947
|
+
|
|
948
|
+
## Card preview
|
|
949
|
+
|
|
950
|
+
\`\`\`markdown
|
|
951
|
+
${CARD_BODY}
|
|
952
|
+
\`\`\`
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
Generated by: \`/optimize\` Phase 6 — contribution path
|
|
956
|
+
Source repo: $(yq '.project.owner + "/" + .project.repo' "$FORGE_YAML" 2>/dev/null || echo 'unknown')
|
|
957
|
+
**This draft will not auto-merge. A human must approve and merge.**
|
|
958
|
+
CONTRIB_PR_EOF
|
|
959
|
+
)" 2>/dev/null) || true
|
|
960
|
+
|
|
961
|
+
if [ -n "$DRAFT_PR_URL" ]; then
|
|
962
|
+
echo "Phase 6: Draft contribution PR opened for $SLUG → $DRAFT_PR_URL"
|
|
963
|
+
echo " ACTION REQUIRED: Review the card, verify no proprietary content, then approve and merge."
|
|
964
|
+
else
|
|
965
|
+
echo "Phase 6: WARN — could not open draft PR for $SLUG. Push the branch manually."
|
|
966
|
+
fi
|
|
967
|
+
done
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### 6D: Phase 6 summary
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
echo ""
|
|
974
|
+
echo "--- Phase 6: Contribution Path Summary ---"
|
|
975
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
976
|
+
echo " Mode: DRY RUN — no PRs opened"
|
|
977
|
+
echo " Generalizable candidates: ${#GENERALIZABLE_CANDIDATES[@]}"
|
|
978
|
+
else
|
|
979
|
+
echo " Exchange target: $EXCHANGE_REPO"
|
|
980
|
+
echo " Candidates evaluated: ${#GENERALIZABLE_CANDIDATES[@]}"
|
|
981
|
+
echo " IMPORTANT: All contribution PRs are DRAFTS — human approval required before merge."
|
|
982
|
+
fi
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
404
987
|
## Idempotency Guarantee
|
|
405
988
|
|
|
406
989
|
Running `/optimize` twice produces the same output. Specifically:
|
|
@@ -408,5 +991,7 @@ Running `/optimize` twice produces the same output. Specifically:
|
|
|
408
991
|
- `registry.json` is always overwritten with current analysis results
|
|
409
992
|
- Generated timestamps in scripts and registry will update on each run (this is expected)
|
|
410
993
|
- Content of scripts changes only if the underlying patterns change
|
|
994
|
+
- Phase 5 is idempotent: existing gate.d scripts and learned-rules entries are detected by slug and skipped (not duplicated)
|
|
995
|
+
- Phase 6 is idempotent: a card slug already present in the exchange repo is detected and skipped (no duplicate PR)
|
|
411
996
|
|
|
412
997
|
To verify: run `/optimize` twice and compare outputs — the only differences should be the `generated_at` timestamps.
|