forgedock 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/review-pr.md +52 -6
- package/commands/work-on/remediate.md +1 -1
- package/commands/work-on/review.md +18 -2
- package/docs/RELEASE-NOTES.md +11 -0
- package/package.json +1 -1
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/classify-finding.sh +26 -2
- package/scripts/classify-finding.test.sh +17 -0
package/commands/review-pr.md
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
1724
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.10.0",
|
|
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"
|
|
@@ -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 `
|
|
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
|