forgedock 1.1.7 → 1.1.9

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 (97) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine/invariants.mjs +361 -0
  4. package/bin/engine-cli.mjs +137 -11
  5. package/bin/env-detect.mjs +249 -0
  6. package/bin/forge-utils.mjs +364 -4
  7. package/bin/forgedock.mjs +1064 -135
  8. package/bin/hooks/pre-tool-use.mjs +182 -0
  9. package/bin/hooks/session-start.mjs +68 -3
  10. package/bin/init-detect.mjs +101 -12
  11. package/bin/journey.mjs +917 -16
  12. package/bin/labels.json +10 -0
  13. package/bin/recall.mjs +557 -0
  14. package/bin/registry.mjs +91 -3
  15. package/bin/runner.mjs +38 -2
  16. package/bin/tui.mjs +49 -3
  17. package/commands/adopt.md +1 -1
  18. package/commands/analytics.md +8 -8
  19. package/commands/audit.md +1 -1
  20. package/commands/autopilot.md +958 -77
  21. package/commands/ci-audit.md +1 -1
  22. package/commands/cleanup.md +139 -55
  23. package/commands/compat-audit.md +1 -1
  24. package/commands/deploy-info.md +1 -1
  25. package/commands/deploy-pr.md +1 -1
  26. package/commands/diagnose.md +1 -1
  27. package/commands/explain.md +1 -1
  28. package/commands/fix-ci.md +1 -1
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/incident-response.md +3 -3
  31. package/commands/issue.md +1 -1
  32. package/commands/optimize.md +586 -1
  33. package/commands/orchestrate/config.md +13 -3
  34. package/commands/orchestrate/phase-1-resolve.md +36 -26
  35. package/commands/orchestrate/phase-2-triage.md +1 -1
  36. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  37. package/commands/orchestrate/phase-3-dependency.md +587 -48
  38. package/commands/orchestrate/phase-4-execution.md +1031 -57
  39. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  40. package/commands/orchestrate/phase-6-report.md +248 -11
  41. package/commands/orchestrate/safety.md +2 -2
  42. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  43. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  44. package/commands/qa-sweep.md +1 -1
  45. package/commands/quality-gate.md +418 -1
  46. package/commands/recover-orphans.md +5 -5
  47. package/commands/replay.md +1 -1
  48. package/commands/review-pr-agents/api.md +2 -2
  49. package/commands/review-pr-agents/auth.md +2 -2
  50. package/commands/review-pr-agents/billing.md +2 -2
  51. package/commands/review-pr-agents/concurrency.md +2 -2
  52. package/commands/review-pr-agents/database.md +2 -2
  53. package/commands/review-pr-agents/frontend.md +2 -2
  54. package/commands/review-pr-agents/infra.md +2 -2
  55. package/commands/review-pr-agents/protocols.md +1 -1
  56. package/commands/review-pr-agents/scraper.md +2 -2
  57. package/commands/review-pr-agents/security.md +2 -2
  58. package/commands/review-pr-staging.md +41 -3
  59. package/commands/review-pr.md +462 -19
  60. package/commands/rollback.md +1 -1
  61. package/commands/scope.md +1 -1
  62. package/commands/security-audit.md +1 -1
  63. package/commands/spec-doctor.md +429 -0
  64. package/commands/test-gate.md +1 -1
  65. package/commands/validate.md +53 -1
  66. package/commands/work-on/build/architect.md +78 -1
  67. package/commands/work-on/build/context.md +360 -22
  68. package/commands/work-on/build/implement.md +3 -1
  69. package/commands/work-on/build/validate.md +29 -1
  70. package/commands/work-on/build.md +75 -6
  71. package/commands/work-on/close.md +624 -10
  72. package/commands/work-on/decompose.md +1 -1
  73. package/commands/work-on/investigate.md +235 -5
  74. package/commands/work-on/remediate.md +331 -0
  75. package/commands/work-on/review.md +1 -1
  76. package/commands/work-on-monolithic.md +1 -1
  77. package/commands/work-on.md +205 -66
  78. package/docs/CI.md +2 -2
  79. package/docs/CODEX.md +1 -1
  80. package/docs/CONFIG.md +186 -0
  81. package/docs/FORGE-PROTOCOL.md +101 -0
  82. package/docs/site/command-reference.md +31 -1
  83. package/docs/site/getting-started.md +9 -34
  84. package/docs/site/index.md +1 -1
  85. package/docs/spec/forge-protocol-v1.md +97 -5
  86. package/docs/spec/label-state-machine.md +6 -1
  87. package/package.json +3 -2
  88. package/packages/protocol/src/cli.js +374 -84
  89. package/packages/protocol/src/types.js +75 -1
  90. package/scripts/build-knowledge-index.mjs +1761 -0
  91. package/scripts/calibration.mjs +996 -0
  92. package/scripts/check-command-docs-drift.sh +4 -2
  93. package/scripts/danger-zones.mjs +789 -0
  94. package/scripts/doctor-pipeline-state.sh +88 -2
  95. package/scripts/select-fix-targets.sh +234 -0
  96. package/scripts/transition-label.sh +245 -6
  97. package/scripts/refresh-bot-token.sh +0 -97
@@ -10,7 +10,7 @@ install: extras
10
10
 
11
11
  Proactively analyze the current repository and generate `.forgedock/scripts/` entries from `forge.yaml → learned:`, git commit history, and pipeline annotations — without waiting for a correction event. This is the proactive half of the learning loop; reactive capture happens in `/work-on` Phase 1D.
12
12
 
13
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
13
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
14
14
  **NEVER use plan mode (EnterPlanMode).**
15
15
 
16
16
  ---
@@ -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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- install: internal
2
+ install: core
3
3
  ---
4
4
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
5
5
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -22,7 +22,9 @@ You are the top-level orchestrator. Your job is to take a batch of issues, plan
22
22
 
23
23
  **You have access to ALL tools** — Agent tool (critical), Task tool, Skill tool, Bash, everything. Use the Agent tool aggressively to parallelize work.
24
24
 
25
- **Agent model policy**: `model: "haiku"`, `effort: low` (mechanical — dispatch bookkeeping, lane routing, classification). Fallback: `model: "sonnet"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
25
+ **Agent model policy**: `model: "haiku"`, `effort: low` (mechanical — dispatch bookkeeping, lane routing, classification). Fallback: `model: "sonnet"` if rate-limited. User can override with `--model <name>`. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. **Distinct from the spawned `/work-on` sub-agents** (Phase 4, per-issue and review-finding sweep): those resolve via `model: "{SUBAGENT_MODEL}"` — forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"` — not this dispatcher's own `haiku` policy.
26
+
27
+ **Hard exclusion — already enforced by this design, not left to convention**: this `haiku`/`effort: low` tier applies ONLY to the orchestrator's own mechanical bookkeeping (dispatch, lane routing, classification) — never to the spawned `/work-on` sub-agents, which always run at `{SUBAGENT_MODEL}` (standard tier, sonnet by default). Every reasoning-heavy `/work-on` sub-phase file (`investigate.md`, `build/architect.md`, `build/implement.md`, `review.md`) carries its own "Agent model policy" line pinned to `{DEFAULT_MODEL}`/standard tier — see `work-on.md`'s `FORGE:MODEL_TIER_NOTE` for the canonical explanation of why these are never downtiered, and forge#1827 for the explicit correction against adding a `model: "haiku"` claim to any file where mechanical bits are interleaved with reasoning-heavy content. Do not introduce a second, competing deterministic-step-to-cheap-model config — this per-file tiering plus the dispatcher/sub-agent split above is the single source of truth for which steps may use a cheaper model.
26
28
  **NEVER use plan mode (EnterPlanMode).**
27
29
 
28
30
  <!-- FORGE:SPEC_LOADED — orchestrate.md loaded and active. Agent is bound by HARD RULES above. -->
@@ -44,9 +46,17 @@ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
44
46
  DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
45
47
  # Build satellite repo map from repos.satellites list
46
48
  # Each satellite: { prefix, repo, staging_branch }
49
+
50
+ # Narration verbosity for Step 4B's per-completion status updates (phase-4-execution.md item 8).
51
+ # "terse" (default when unset): one-line status per completion — matches today's existing
52
+ # behavior, so leaving pipeline.narration unset is a no-op. "verbose": adds a running
53
+ # per-completion recap table. Never affects the Step 6B consolidated report, which always
54
+ # renders full tables once at the end regardless of this value.
55
+ NARRATION_MODE=$(yq '.pipeline.narration // "terse"' "$CONFIG_FILE" 2>/dev/null || echo "terse")
56
+ [ "$NARRATION_MODE" = "null" ] && NARRATION_MODE="terse"
47
57
  ```
48
58
 
49
- All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, and `{DEFAULT_BRANCH}` references below are populated from `forge.yaml`.
59
+ All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, `{DEFAULT_BRANCH}`, and `{NARRATION_MODE}` references below are populated from `forge.yaml`.
50
60
 
51
61
  ---
52
62