forgedock 1.9.1 → 1.10.1

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.
@@ -422,7 +422,7 @@ if [ -n "$FORGE_ROOT" ] && [ -r "$FORGE_ROOT/bin/engine/admission.mjs" ]; then
422
422
  import(require("node:url").pathToFileURL(process.argv[1]).href).then(({ planP3BatchGroups, summarizeP3BatchPlan }) => {
423
423
  const plan = planP3BatchGroups(JSON.parse(process.argv[2]), undefined, { openBatches: JSON.parse(process.argv[3]) });
424
424
  console.log(JSON.stringify({ plan, summary: summarizeP3BatchPlan(plan) }));
425
- }).catch((e) => { console.error("WARNING: planP3Batches failed: " + e.message); process.exit(1); });
425
+ }).catch((e) => { console.error("WARNING: planP3Batches failed: " + ((e && e.message) || String(e))); process.exit(1); });
426
426
  ' "$FORGE_ROOT/bin/engine/admission.mjs" "$BATCH_CANDIDATE_REGISTRY_JSON" "$OPEN_BATCHES_JSON") || BATCH_PLAN=""
427
427
  if [ -z "$BATCH_PLAN" ]; then
428
428
  echo "WARNING: P3 batch plan empty (admission.mjs import or JSON parse failed) — candidates retained as ungrouped singletons" >&2
@@ -1717,11 +1717,26 @@ FINDING_LINEAGE="none"
1717
1717
  if [ -n "${MERGE_ISSUE:-}" ] && gh issue view "$MERGE_ISSUE" -R {GH_REPO} --json labels --jq '.labels[].name' 2>/dev/null | grep -qx 'review-finding'; then
1718
1718
  FINDING_LINEAGE="review-finding"
1719
1719
  fi
1720
- # Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths):
1720
+ # In-PR fix gate (§6B.6): only for an auto-merge review with a linked issue, and only for the first
1721
+ # fix round on this PR. Any trusted FORGE:INPR_FIX* marker already on the PR means the round is used.
1722
+ INPR_DIFF_FILE=""
1723
+ if [ "${AUTO_MERGE:-false}" = "true" ] && [ -n "${MERGE_ISSUE:-}" ]; then
1724
+ INPR_MARKERS=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null \
1725
+ | jq -s '[.[][] | select(.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") | select(.body | test("^<!-- FORGE:INPR_FIX"))] | length' 2>/dev/null || echo "")
1726
+ if [ "$INPR_MARKERS" = "0" ]; then
1727
+ mkdir -p "${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
1728
+ INPR_DIFF_FILE=$(mktemp "${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/{PR_NUMBER}_inpr-diff.XXXXXX")
1729
+ gh pr diff {PR_NUMBER} -R {GH_REPO} --name-only > "$INPR_DIFF_FILE" 2>/dev/null || { rm -f "$INPR_DIFF_FILE"; INPR_DIFF_FILE=""; }
1730
+ fi
1731
+ # An unreadable marker list or diff leaves INPR_DIFF_FILE empty: findings are filed as before (never gated blind).
1732
+ fi
1733
+ # Per deduped finding (FINDING_TEXT_FILE = mktemp file holding title + body + affected paths;
1734
+ # FINDING_FILE = the finding's file path, file:line accepted):
1721
1735
  # DISPOSITION=$(bash "$CLASSIFY_SCRIPT" --severity "$FINDING_SEVERITY" --confidence "$FINDING_CONFIDENCE" \
1722
- # --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE")
1723
- # case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; esac # first word is the class, the rest is the reason
1724
- # If CLASSIFY_SCRIPT is empty, apply the four rules above by hand and record classifier=manual below.
1736
+ # --agent "$FINDING_AGENT" --lineage "$FINDING_LINEAGE" --text-file "$FINDING_TEXT_FILE" \
1737
+ # ${INPR_DIFF_FILE:+--inpr-diff "$INPR_DIFF_FILE" --file "$FINDING_FILE"})
1738
+ # case "$DISPOSITION" in ISSUE*) ;; NOTE*) ;; INPR_FIX*) ;; esac # first word is the class, the rest is the reason
1739
+ # If CLASSIFY_SCRIPT is empty, apply the four rules above (and §6B.6) by hand and record classifier=manual below.
1725
1740
  ```
1726
1741
 
1727
1742
  Each NOTE gets exactly one disposition, in this preference order:
@@ -1731,17 +1746,38 @@ Each NOTE gets exactly one disposition, in this preference order:
1731
1746
 
1732
1747
  **Metrics note (forge#3106)**: because NOTEs are not filed as issues, `review-finding` issue volume and any metric derived from it (findings per PR, `/pipeline-health` finding rates, amplification ratio) drop relative to pre-#3060 history. Compare against the `notes_*` counts in the review summary, not issue counts alone, when judging review depth across the change.
1733
1748
 
1734
- Every NOTE disposition must be recorded (never silently dropped): list each NOTE (id, file:line, one-line reason) with its disposition in the review summary comment, including dropped ones, so a reviewer can audit the decision. NOTES are never passed to `Skill(issue)`. Record counts in the review summary (`notes_fixed`, `notes_listed`, `notes_dropped`, `findings_filed`). If no finding is an ISSUE after this step, skip 6C and continue to Phase 7.
1749
+ Every NOTE disposition must be recorded (never silently dropped): list each NOTE (id, file:line, one-line reason) with its disposition in the review summary comment, including dropped ones, so a reviewer can audit the decision. NOTES are never passed to `Skill(issue)`. Record counts in the review summary (`notes_fixed`, `notes_listed`, `notes_dropped`, `findings_filed`, `inpr_fix`). If no finding is an ISSUE after this step, skip 6C and continue to Phase 7. Findings classified `INPR_FIX` (§6B.6) are neither notes nor filed this round.
1735
1750
 
1736
1751
  **Disposition marker (MANDATORY whenever Phase 6A extracted at least one finding):** post one PR comment whose first line is the machine-readable record below, followed by the NOTE list. Phase 8 refuses to auto-merge a PR that has `FINDING:` markers but no `FORGE:NOTE_DISPOSITION` comment — the disposition step is enforced, not advisory.
1737
1752
 
1738
1753
  ```bash
1739
1754
  CLASSIFIER_MODE=$([ -n "$CLASSIFY_SCRIPT" ] && echo script || echo manual)
1740
1755
  # NOTE_LIST_FILE: mktemp file named for the PR number, holding one line per NOTE (id, file:line, disposition, reason).
1741
- gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=${NOTES_FIXED:-0} notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} lineage=${FINDING_LINEAGE} classifier=${CLASSIFIER_MODE} -->
1756
+ gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=${NOTES_FIXED:-0} notes_listed=${NOTES_LISTED:-0} notes_dropped=${NOTES_DROPPED:-0} findings_filed=${FINDINGS_FILED:-0} inpr_fix=${INPR_FIX_COUNT:-0} lineage=${FINDING_LINEAGE} classifier=${CLASSIFIER_MODE} -->
1742
1757
  $(cat "$NOTE_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
1743
1758
  ```
1744
1759
 
1760
+ ### 6B.6: In-PR fix gate for CONFIRMED MEDIUM findings in the PR's own files (#3387, narrowed)
1761
+
1762
+ A CONFIRMED MEDIUM defect in a file this PR changed is cheaper to fix before merge than through a separate `/work-on` cycle, and merging it knowingly ships the defect. A finding classified `INPR_FIX` is therefore fixed on the PR instead of filed. The gate is deliberately narrow:
1763
+
1764
+ - **Only `MEDIUM` + `CONFIRMED`.** LIKELY and POSSIBLE are not gated. HIGH/CRITICAL already withhold APPROVED through §7B.
1765
+ - **Only files in the PR diff** (`gh pr diff --name-only`). Pre-existing defects elsewhere are filed as before.
1766
+ - **One fix round per PR.** Once any `FORGE:INPR_FIX*` marker is on the PR, later reviews classify normally: anything still present is filed as an issue and the PR merges.
1767
+ - **Never stops at `needs-human` because of this gate.** If the fix round fails, `/work-on` waives the gate for the current head and the findings are filed (work-on/review.md R4).
1768
+ - **Auto-merge reviews with `--issue` only.** A standalone review without `--auto-merge` files findings as before.
1769
+
1770
+ When at least one finding is `INPR_FIX`, do NOT file those findings in 6C. Post the request on the PR as one comment, after the §6B.5 disposition marker. Phase 8 then refuses to merge this head and returns `REVIEW_RESULT: status: BLOCKED`, blocker `in-pr fix required`:
1771
+
1772
+ ```bash
1773
+ INPR_HEAD=$(gh pr view {PR_NUMBER} -R {GH_REPO} --json headRefOid --jq .headRefOid)
1774
+ # INPR_FIX_IDS: comma-separated finding ids classified INPR_FIX. INPR_FIX_LIST_FILE: mktemp file, one line per finding:
1775
+ # id, file:line, one-sentence problem, suggested fix — this is the remediation work order.
1776
+ gh pr comment {PR_NUMBER} -R {GH_REPO} --body "<!-- FORGE:INPR_FIX: round=1 head=${INPR_HEAD} findings=${INPR_FIX_IDS} -->
1777
+ In-PR fix requested before merge (CONFIRMED MEDIUM findings in files this PR changed). One fix round; anything still present on the next review is filed as an issue.
1778
+ $(cat "$INPR_FIX_LIST_FILE" 2>/dev/null)" # allowlist:check-command-side-effects
1779
+ ```
1780
+
1745
1781
  ### 6C: Create Issues
1746
1782
 
1747
1783
  ```bash
@@ -2259,6 +2295,16 @@ if [ "${FINDING_COUNT:-0}" -gt 0 ] && [ "${DISPOSITION_COUNT:-0}" -eq 0 ]; then
2259
2295
  # STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition missing".
2260
2296
  exit 1
2261
2297
  fi
2298
+ # In-PR fix gate (§6B.6): an unanswered INPR_FIX request at the current head blocks the merge until the fix
2299
+ # round runs (a new head) or /work-on waives it for this head. Only trusted authors' markers count.
2300
+ PR_HEAD_NOW=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json headRefOid --jq .headRefOid 2>/dev/null || echo "")
2301
+ INPR_REQ=$(printf '%s' "$DISPO_JSON" | jq -s -r '[.[][] | select(.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") | select(.body | test("^<!-- FORGE:INPR_FIX: ")) | .body | capture("head=(?<h>[0-9a-f]{7,40})").h] | last // ""')
2302
+ INPR_WAIVED=$(printf '%s' "$DISPO_JSON" | jq -s -r --arg h "$PR_HEAD_NOW" '[.[][] | select(.author_association == "OWNER" or .author_association == "MEMBER" or .author_association == "COLLABORATOR") | select(.body | test("^<!-- FORGE:INPR_FIX_WAIVED: head=" + $h))] | length')
2303
+ if [ -n "$INPR_REQ" ] && [ "$INPR_REQ" = "$PR_HEAD_NOW" ] && [ "${INPR_WAIVED:-0}" -eq 0 ]; then
2304
+ echo "IN-PR FIX: CONFIRMED MEDIUM findings in this PR's files were requested at head ${PR_HEAD_NOW} — fix them before merging (§6B.6)" >&2
2305
+ # STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "in-pr fix required". Not needs-human: /work-on R4 runs one fix round.
2306
+ exit 1
2307
+ fi
2262
2308
  ```
2263
2309
 
2264
2310
  **Phase-trail preflight (MANDATORY before any merge attempt)** <!-- Added: forge#3061 -->: a reviewer verdict alone must not merge work whose earlier pipeline phases were skipped. Run the deterministic verifier against the linked issue; do NOT run the merge block below unless it exits 0.
@@ -112,7 +112,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
112
112
  ```
113
113
 
114
114
  **Classify into FIXABLE vs. UNFIXABLE**:
115
- - **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a mergeability guard failure (`CONFLICTING`/`DIRTY`/`BLOCKED` — resolvable by rebasing onto `{PR_BASE}`), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason).
115
+ - **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a mergeability guard failure (`CONFLICTING`/`DIRTY`/`BLOCKED` — resolvable by rebasing onto `{PR_BASE}`), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
116
116
  - **UNFIXABLE (policy escalation)** — `HAS_PURPOSE_REGRESSION=true` (the PR's behavior diverges from the issue's intent — a judgment call, not a code defect), `CALIBRATION_NEEDS_HUMAN=true` (statistical trust threshold), or `TRUST_NEEDS_HUMAN=true` (provenance `NOVEL_NEEDS_HUMAN` tier, insufficient prior data — a policy gate, not a bug). None of these are mechanically "fixable" by re-editing code.
117
117
 
118
118
  **If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (it should already be present), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
@@ -534,12 +534,26 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
534
534
 
535
535
  - `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "ci gate" (Phase 8 refused to merge because checks failed, were cancelled, or stayed pending past the gate retries): do NOT attempt the manual merge below — that would bypass the CI gate. Fixing red CI is pipeline work, not a human decision: invoke remediation **once** — `Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {NUMBER} --base {PR_BASE}")` (forked; it classifies a CI-gate refusal as FIXABLE, clears `needs-human`, reads the failing job logs, fixes them on the PR branch, re-runs the quality gate and a full review, and auto-lands through the same CI gate). Bound: count `<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->` comments on the issue first; if ≥ 1, do not remediate again. Post that marker before invoking. `REMEDIATE_RESULT` re-gate outcome `AUTO-LANDED` → treat as merged and return `REVIEW_RESULT: status: COMPLETE`; `REMEDIATE_RESULT: status: REREVIEW_REQUIRED` (forge#3240: the fix is pushed but remediation had no sub-agent dispatch tool, so no review ran) → do NOT add `needs-human` and do not re-invoke remediation; return `REVIEW_RESULT: status: BLOCKED` with blocker "re-review required: no dispatch tool" so the caller re-dispatches review from a session that has dispatch; any other outcome → leave `needs-human` (remediation sets it) and return `REVIEW_RESULT: status: BLOCKED` with blocker "ci gate not green after remediation". <!-- forge#3191 -->
536
536
 
537
- Persisted loop bounds for the two cases above — count first, post the marker, then act (`{BOUND}` is `STALE_REREVIEW` or `CI_REMEDIATION`):
537
+ - `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "in-pr fix required" (review-pr §6B.6: CONFIRMED MEDIUM findings in files this PR changed, requested on the PR in a `FORGE:INPR_FIX` comment at the current head): do NOT merge and do NOT leave the issue at `needs-human` because of this gate. Run **one** fix round, bounded by `<!-- FORGE:INPR_REMEDIATION: pr={PR_NUMBER} -->` on the issue (`{BOUND}` = `INPR_REMEDIATION` below).
538
+ - **Bound unused**: post the marker. Add `needs-human` (remediation only targets `needs-human`-gated PRs, and its Phase M1 clears it as FIXABLE). Then invoke `Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {NUMBER} --base {PR_BASE}")`. Remediation reads the `FORGE:INPR_FIX` work order, fixes exactly those findings, re-reviews the new head and auto-lands. On the re-review, any finding still present is filed as an issue (the round is used).
539
+ - `REMEDIATE_RESULT` re-gate outcome `AUTO-LANDED` → return `REVIEW_RESULT: status: COMPLETE`. `REREVIEW_REQUIRED` → same handling as the ci-gate case above.
540
+ - **Any other outcome, or bound already used**: waive the gate for the current head. Remove `needs-human`, post `<!-- FORGE:INPR_FIX_WAIVED: head=<current head SHA> -->` on the **PR**, then re-invoke Phase R3 once under the `STALE_REREVIEW` bound. That review files the remaining findings as issues and merges as it would have before this gate. Only if `STALE_REREVIEW` is also exhausted, add `needs-human`.
541
+ ```bash
542
+ INPR_WAIVE_HEAD=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq .headRefOid)
543
+ gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
544
+ gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:INPR_FIX_WAIVED: head=${INPR_WAIVE_HEAD} -->
545
+ In-PR fix round did not land; the remaining CONFIRMED MEDIUM findings are filed as issues on the next review instead of blocking the merge." # allowlist:check-command-side-effects
546
+ ```
547
+
548
+ Persisted loop bounds for the cases above — count first, post the marker, then act (`{BOUND}` is `STALE_REREVIEW`, `CI_REMEDIATION` or `INPR_REMEDIATION`):
538
549
  ```bash
539
550
  [ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
540
551
  if [ "{BOUND}" = "STALE_REREVIEW" ]; then
541
552
  BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
542
553
  --jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
554
+ elif [ "{BOUND}" = "INPR_REMEDIATION" ]; then
555
+ BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
556
+ --jq '[.[] | select((.body | contains("FORGE:INPR_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
543
557
  else
544
558
  BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
545
559
  --jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$1} END {print s+0}')
@@ -551,11 +565,13 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
551
565
  Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head once." 2>/dev/null || true # when {BOUND}=STALE_REREVIEW
552
566
  gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->
553
567
  CI gate refused PR #{PR_NUMBER}: dispatching remediation once to fix the failing checks." 2>/dev/null || true # when {BOUND}=CI_REMEDIATION
568
+ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INPR_REMEDIATION: pr={PR_NUMBER} -->
569
+ In-PR fix requested on PR #{PR_NUMBER}: dispatching remediation once to fix the CONFIRMED MEDIUM findings before merge." 2>/dev/null || true # when {BOUND}=INPR_REMEDIATION
554
570
  fi
555
571
  ```
556
572
  Post only the comment matching `{BOUND}`. `BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
557
573
 
558
- - PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review or ci-gate BLOCKED above) → attempt manual merge, **only if the PR head is still the commit named in the latest `<!-- FORGE:REVIEW -->` APPROVED verdict** (otherwise re-invoke Phase R3 instead, as for "stale review") **and only after the same CI gate**:
574
+ - PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review, ci-gate or in-pr-fix BLOCKED above) → attempt manual merge, **only if the PR head is still the commit named in the latest `<!-- FORGE:REVIEW -->` APPROVED verdict** (otherwise re-invoke Phase R3 instead, as for "stale review") **and only after the same CI gate**:
559
575
  ```bash
560
576
  # CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
561
577
  # green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
@@ -2,6 +2,17 @@
2
2
 
3
3
  Operator-facing notes for behavior changes that need action or awareness. Newest first.
4
4
 
5
+ ## In-PR fix round for CONFIRMED MEDIUM findings (#3387, narrowed)
6
+
7
+ Under `--auto-merge` with `--issue` (the `/work-on` pipeline), a review finding that is **MEDIUM + CONFIRMED** in a file the PR itself changed is no longer merged and filed as a follow-up issue. `/review-pr` §6B.6 posts a `FORGE:INPR_FIX` work order on the PR, and Phase 8 holds the merge for that head (`blocker: in-pr fix required`). `/work-on` review then runs **one** remediation round (bound: `FORGE:INPR_REMEDIATION` on the issue) that fixes exactly those findings and re-reviews.
8
+
9
+ - **Narrow by design.** LIKELY/POSSIBLE, findings outside the PR's changed files, and standalone reviews are unchanged. HIGH/CRITICAL already block through §7B.
10
+ - **One round only.** Anything still present on the re-review is filed as an issue, as before.
11
+ - **Never a `needs-human` stop because of this gate.** If the round does not land, `/work-on` posts `FORGE:INPR_FIX_WAIVED` for the current head and re-reviews once, so the findings are filed and the PR merges as it did before.
12
+ - **A missing finding path never gates.** If a finding has no file path, or the PR diff cannot be read, it is filed as before.
13
+
14
+ Action: none. Expect a few PRs to take one extra fix round before merging, and fewer MEDIUM `review-finding` issues.
15
+
5
16
  ## Cascade follow-ups: script resolution, domain-agent notes, verification in worktrees, spec bash check
6
17
 
7
18
  - **Script resolution works in consumer repos.** `/review-pr` §6B.5 and the orchestrator breaker now find `classify-finding.sh` and `amplification-breaker.sh` through the newest pinned ForgeDock plugin cache under `CLAUDE_CONFIG_DIR`, then `~/.claude`. They also check `FORGEDOCK_HOME` and the repo's own `scripts/`, as before. Previously the plugin-root placeholder was often left unsubstituted in forked review runs, so 4 of 9 AlterLab reviews fell back to `classifier=manual`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -119,6 +119,9 @@ MARKER_REGISTRY="
119
119
  INDEP_VERIFY
120
120
  INDEP_VERIFY_FAIL
121
121
  INDEP_VERIFY_PASS
122
+ INPR_FIX
123
+ INPR_FIX_WAIVED
124
+ INPR_REMEDIATION
122
125
  INVESTIGATOR
123
126
  JSONL_PARSER_UTIL
124
127
  KNOWLEDGE_GIST
@@ -21,8 +21,13 @@
21
21
  # review finding. Default `none`.
22
22
  # --text/--text-file Finding title + body + affected file paths, used for the
23
23
  # content-based safety exemption.
24
+ # --inpr-diff <F> --file <P> In-PR fix gate (#3387, narrowed): F is a file listing the
25
+ # PR's changed paths, one per line; P is the finding's file path. When both
26
+ # are given, a MEDIUM CONFIRMED finding whose path is in the PR diff is
27
+ # classified INPR_FIX — fix it on the PR before merge instead of filing it.
28
+ # Callers pass these only for the first fix round of an auto-merge review.
24
29
  #
25
- # Output (stdout, one line): `ISSUE <reason>` or `NOTE <reason>`.
30
+ # Output (stdout, one line): `ISSUE <reason>`, `NOTE <reason>` or `INPR_FIX <reason>`.
26
31
  # Exit codes: 0 classified, 2 usage error.
27
32
  #
28
33
  # Rules (forge#3060, tightened after the 2026-10-08 cascade audit):
@@ -50,9 +55,11 @@ CONFIDENCE=""
50
55
  AGENT=""
51
56
  LINEAGE="none"
52
57
  TEXT=""
58
+ INPR_DIFF=""
59
+ FINDING_FILE=""
53
60
 
54
61
  usage() {
55
- echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>]" >&2
62
+ echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>]" >&2
56
63
  exit 2
57
64
  }
58
65
 
@@ -63,6 +70,8 @@ while [ "$#" -gt 0 ]; do
63
70
  --agent) [ "$#" -ge 2 ] || usage; AGENT="$2"; shift 2 ;;
64
71
  --lineage) [ "$#" -ge 2 ] || usage; LINEAGE="$2"; shift 2 ;;
65
72
  --text) [ "$#" -ge 2 ] || usage; TEXT="$2"; shift 2 ;;
73
+ --inpr-diff) [ "$#" -ge 2 ] || usage; INPR_DIFF="$2"; shift 2 ;;
74
+ --file) [ "$#" -ge 2 ] || usage; FINDING_FILE="$2"; shift 2 ;;
66
75
  --text-file)
67
76
  [ "$#" -ge 2 ] || usage
68
77
  [ -r "$2" ] || { echo "ERROR: --text-file not readable: $2" >&2; exit 2; }
@@ -76,6 +85,10 @@ case "$LINEAGE" in
76
85
  *) echo "ERROR: --lineage must be 'none' or 'review-finding' (got '$LINEAGE')" >&2; exit 2 ;;
77
86
  esac
78
87
 
88
+ if [ -n "$INPR_DIFF" ] && [ ! -r "$INPR_DIFF" ]; then
89
+ echo "ERROR: --inpr-diff not readable: $INPR_DIFF" >&2; exit 2
90
+ fi
91
+
79
92
  upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]'; }
80
93
  SEV=$(upper "$SEVERITY")
81
94
  CONF=$(upper "$CONFIDENCE")
@@ -86,6 +99,17 @@ case "$SEV" in
86
99
  *) echo "ISSUE unparseable-severity"; exit 0 ;;
87
100
  esac
88
101
 
102
+ # In-PR fix gate (#3387, narrowed). MEDIUM CONFIRMED is an ISSUE under every rule below
103
+ # (lineage included), so checking here changes only where it is handled, never whether.
104
+ if [ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] && [ "$SEV" = "MEDIUM" ] && [ "$CONF" = "CONFIRMED" ]; then
105
+ _path="${FINDING_FILE#./}"; _path="${_path%%:*}"
106
+ if grep -Fxq -- "$_path" "$INPR_DIFF"; then
107
+ echo "INPR_FIX medium-confirmed-in-diff"
108
+ exit 0
109
+ fi
110
+ fi
111
+
112
+
89
113
  # Safety exemption. Keywords match whole words, where `_`, `-`, `/` and other
90
114
  # punctuation separate words (so `auth_service` matches, `author` does not).
91
115
  SAFETY=""
@@ -66,6 +66,23 @@ printf 'Title: unchecked xss sink\n' > "$TMPF"
66
66
  expect ISSUE "text-file keyword" -- --severity LOW --confidence LIKELY --text-file "$TMPF"
67
67
  rm -f "$TMPF"
68
68
 
69
+ # --- In-PR fix gate (#3387, narrowed): MEDIUM CONFIRMED in the PR diff ---
70
+ DIFF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-diff.XXXXXX")
71
+ printf 'src/api/jobs.py\ncommands/review-pr.md\n' > "$DIFF"
72
+ expect INPR_FIX "MEDIUM CONFIRMED in diff" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
73
+ expect INPR_FIX "file:line form matches" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file "src/api/jobs.py:142"
74
+ expect INPR_FIX "./ prefix matches" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file ./commands/review-pr.md
75
+ expect INPR_FIX "lineage PR still fixes in-PR" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --inpr-diff "$DIFF" --file src/api/jobs.py
76
+ expect ISSUE "MEDIUM CONFIRMED outside diff files" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
77
+ expect ISSUE "MEDIUM LIKELY in diff is not gated" -- --severity MEDIUM --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
78
+ expect ISSUE "HIGH in diff stays ISSUE (7B blocks it)" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
79
+ expect NOTE "LOW in diff stays NOTE" -- --severity LOW --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
80
+ expect ISSUE "no --inpr-diff: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --file src/api/jobs.py
81
+ expect ISSUE "no --file: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF"
82
+ expect ISSUE "prefix of a diff path does not match" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api
83
+ rm -f "$DIFF"
84
+ expect_exit 2 "unreadable --inpr-diff" -- --severity MEDIUM --inpr-diff /nonexistent/d --file x
85
+
69
86
  # --- usage errors ---
70
87
  expect_exit 2 "unknown flag" -- --bogus x
71
88
  expect_exit 2 "bad lineage" -- --severity LOW --lineage P3