forgedock 1.3.0 → 1.5.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/bin/batch-runner.mjs +31 -2
- package/bin/engine/admission.mjs +294 -0
- package/bin/engine/admission.test.mjs +265 -0
- package/bin/engine/invariants.mjs +17 -4
- package/bin/engine/phases.mjs +201 -35
- package/bin/engine/resolve.mjs +256 -0
- package/bin/engine/resolve.test.mjs +149 -0
- package/bin/engine/runlog.mjs +11 -1
- package/bin/engine/state.mjs +20 -28
- package/bin/engine-cli.mjs +86 -5
- package/bin/engine.mjs +543 -36
- package/bin/forgedock.mjs +397 -59
- package/bin/hooks/interactive-engine.mjs +50 -24
- package/bin/hooks/pre-tool-use.mjs +191 -13
- package/bin/journey.mjs +586 -44
- package/bin/labels.json +5 -0
- package/bin/runner.mjs +478 -10
- package/bin/watch-utils.mjs +258 -0
- package/commands/cleanup.md +34 -5
- package/commands/issue.md +14 -4
- package/commands/orchestrate/config.md +43 -1
- package/commands/orchestrate/phase-1-resolve.md +348 -11
- package/commands/orchestrate/phase-3-dependency.md +272 -49
- package/commands/orchestrate/phase-4-execution.md +622 -60
- package/commands/orchestrate/phase-5-cleanup.md +2 -0
- package/commands/quality-gate.md +6 -6
- package/commands/review-pr-staging.md +51 -8
- package/commands/review-pr.md +120 -39
- package/commands/upgrade-deps.md +7 -2
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on/investigate.md +22 -4
- package/commands/work-on/remediate.md +55 -4
- package/commands/work-on/review.md +40 -1
- package/commands/work-on.md +19 -4
- package/docs/CONFIG.md +120 -0
- package/docs/spec/eval-run-result.md +8 -4
- package/docs/spec/label-state-machine.md +9 -0
- package/package.json +2 -2
- package/packages/protocol/src/emit.js +5 -22
- package/packages/protocol/src/html-comment-escape.js +68 -0
- package/packages/protocol/src/index.js +1 -0
- package/packages/protocol/src/parse.js +5 -6
- package/packages/protocol/src/phases.js +105 -0
- package/packages/protocol/src/types.js +17 -0
- package/packages/protocol/src/validate.js +24 -1
- package/scripts/check-admission-parity.mjs +167 -0
- package/scripts/check-command-side-effects.sh +181 -66
- package/scripts/check-phase-registry-drift.mjs +118 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/classify-lane.sh +29 -3
- package/scripts/derive-finding-milestone.sh +184 -0
- package/scripts/doctor-pipeline-state.sh +1 -0
- package/scripts/extract-affected-files.sh +232 -0
- package/scripts/extract-affected-files.test.sh +331 -0
- package/scripts/forge-run.sh +39 -4
- package/scripts/issue-dedup.sh +69 -8
- package/scripts/issue-dedup.test.sh +142 -0
- package/scripts/severity-to-priority.sh +81 -0
|
@@ -45,6 +45,8 @@ Include the audit summary in the final report (Phase 6). Key metrics to surface:
|
|
|
45
45
|
|
|
46
46
|
**Run after Step 5B, before Step 5D.** No-ops cleanly if the claims board was never created this run (`COORD_ISSUE_NUMBER` unset/empty — e.g. Step 3D.1's `gh issue create` failed, or this batch never reached DAG construction). Tolerates GitHub API failures without aborting the rest of Phase 5 — the same tolerant-failure convention (`2>/dev/null || echo ...`, `|| true`) used throughout `phase-3-dependency.md`/`phase-4-execution.md`.
|
|
47
47
|
|
|
48
|
+
**Orchestrator lease should already be released by this point** (forge#2627): `phase-4-execution.md`'s Termination condition (end of Step 4B) calls `release_orchestrator_lease()` before handing off to Phase 5 on a normal drain, and the "Stopping the orchestrator" procedure (Step 4A-pre.-0.5) does the same on an interrupted exit. Closing the coordination issue here does not depend on lease state either way — an unreleased lease on an issue about to be closed is harmless (a future batch creates its own new coordination issue per Step 3D.1), but if `ACTIVE_CLAIMS_NOTE` logic is ever extended to also surface lease state, prefer checking for a still-unreleased `FORGE:LEASE` the same way `ACTIVE_CLAIMS` is computed above.
|
|
49
|
+
|
|
48
50
|
```bash
|
|
49
51
|
if [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
|
|
50
52
|
# Detect any still-active (unreleased) claims — a claim posted with no matching
|
package/commands/quality-gate.md
CHANGED
|
@@ -1226,7 +1226,7 @@ fi
|
|
|
1226
1226
|
|
|
1227
1227
|
**Triggered when**: BILLING domain is set (changed files are under `billing/`, `credits/`, `subscription`, `ledger`, or `payment` paths).
|
|
1228
1228
|
|
|
1229
|
-
**Why this matters**: Billing code has three reliably grep-detectable defect classes that
|
|
1229
|
+
**Why this matters**: Billing code has three reliably grep-detectable defect classes that recur often enough in production billing code to justify a deterministic check: (1) `org_id` not propagated to debit/refund ledger write paths (silent data ownership loss — charges appear with wrong tenant); (2) `asyncio.sleep` / blocking sleep called inside an advisory-lock or open-transaction scope (holds DB connection/lock for the full sleep duration, causing connection exhaustion and deadlocks under load); (3) stdlib `logging.*()` calls passed structlog-style keyword arguments (`event=`, named context keys) that are not valid stdlib kwargs — crashes at runtime with `TypeError: unexpected keyword argument`. <!-- Added: forge#1329 -->
|
|
1230
1230
|
|
|
1231
1231
|
**2Q-1: org_id propagation in ledger write functions**
|
|
1232
1232
|
|
|
@@ -1241,7 +1241,7 @@ while IFS= read -r f; do
|
|
|
1241
1241
|
# Read the full function signature (up to 3 lines to cover multi-line sigs)
|
|
1242
1242
|
sig=$(sed -n "${lineno},$((lineno+2))p" "$f" 2>/dev/null)
|
|
1243
1243
|
echo "$sig" | grep -qE '\borg_id\b' || \
|
|
1244
|
-
echo "BILLING-1 | MEDIUM | $f:$lineno | Ledger write function '$(echo "$rest" | grep -oE 'def \w+' | head -1)' signature does not include org_id parameter — verify org_id is propagated to all debit/refund paths
|
|
1244
|
+
echo "BILLING-1 | MEDIUM | $f:$lineno | Ledger write function '$(echo "$rest" | grep -oE 'def \w+' | head -1)' signature does not include org_id parameter — verify org_id is propagated to all debit/refund paths"
|
|
1245
1245
|
done
|
|
1246
1246
|
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
1247
1247
|
```
|
|
@@ -1252,14 +1252,14 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
|
1252
1252
|
# Grep billing Python files for asyncio.sleep / time.sleep called inside an advisory-lock
|
|
1253
1253
|
# or open-transaction context (within 20 lines after lock acquisition or BEGIN/session open).
|
|
1254
1254
|
# Holding a lock or DB connection across a sleep starves the connection pool and causes
|
|
1255
|
-
# deadlocks under concurrent load
|
|
1255
|
+
# deadlocks under concurrent load.
|
|
1256
1256
|
while IFS= read -r f; do
|
|
1257
1257
|
[ -z "$f" ] && continue
|
|
1258
1258
|
grep -nE "\basyncio\.sleep\b|\btime\.sleep\b" "$f" 2>/dev/null | while IFS=: read -r lineno rest; do
|
|
1259
1259
|
start=$((lineno > 20 ? lineno - 20 : 1))
|
|
1260
1260
|
window=$(sed -n "${start},$((lineno-1))p" "$f" 2>/dev/null)
|
|
1261
1261
|
echo "$window" | grep -qE 'advisory_lock|BEGIN\b|async with.*session|async with.*conn|async with.*transaction|with.*engine\b' && \
|
|
1262
|
-
echo "BILLING-2 | HIGH | $f:$lineno | asyncio.sleep/time.sleep inside advisory-lock or open-transaction scope — holds DB connection/lock for full sleep duration, causing connection exhaustion and deadlocks under load
|
|
1262
|
+
echo "BILLING-2 | HIGH | $f:$lineno | asyncio.sleep/time.sleep inside advisory-lock or open-transaction scope — holds DB connection/lock for full sleep duration, causing connection exhaustion and deadlocks under load. Move sleep outside lock/transaction scope."
|
|
1263
1263
|
done
|
|
1264
1264
|
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
1265
1265
|
```
|
|
@@ -1271,7 +1271,7 @@ done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
|
1271
1271
|
# that are NOT valid stdlib logging kwargs (exc_info, stack_info, stacklevel, extra).
|
|
1272
1272
|
# Structlog-style kwargs (event=, extra_=, named context keys) are NOT accepted by stdlib
|
|
1273
1273
|
# Logger methods and crash at runtime with TypeError: unexpected keyword argument.
|
|
1274
|
-
#
|
|
1274
|
+
# This is a recurring, well-observed pattern in production billing code.
|
|
1275
1275
|
while IFS= read -r f; do
|
|
1276
1276
|
[ -z "$f" ] && continue
|
|
1277
1277
|
# Match logging.info/warning/error/debug/critical and logger.info/etc. calls with
|
|
@@ -1280,7 +1280,7 @@ while IFS= read -r f; do
|
|
|
1280
1280
|
grep -nE "\b(logging|logger)\.(info|warning|error|debug|critical|exception)\s*\(.*\b\w+\s*=" "$f" 2>/dev/null | \
|
|
1281
1281
|
grep -vE "\b(exc_info|stack_info|stacklevel|extra)\s*=" | \
|
|
1282
1282
|
grep -v "^\s*#" && \
|
|
1283
|
-
echo "BILLING-3 | HIGH | $f | stdlib logging call with non-stdlib keyword argument detected — stdlib Logger.info/warning/error/etc. only accept exc_info, stack_info, stacklevel, extra as kwargs. Structlog-style kwargs (event=, named context keys) cause TypeError at runtime
|
|
1283
|
+
echo "BILLING-3 | HIGH | $f | stdlib logging call with non-stdlib keyword argument detected — stdlib Logger.info/warning/error/etc. only accept exc_info, stack_info, stacklevel, extra as kwargs. Structlog-style kwargs (event=, named context keys) cause TypeError at runtime. Use structlog.get_logger() instead of logging if structlog kwargs are needed."
|
|
1284
1284
|
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
1285
1285
|
```
|
|
1286
1286
|
|
|
@@ -681,14 +681,32 @@ gh label create "needs-validation" --color "FBCA04" --description "Review findin
|
|
|
681
681
|
gh label create "staging-review" --color "1D76DB" --description "Finding from a staging branch review before deploy to main. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
682
682
|
```
|
|
683
683
|
|
|
684
|
-
### 7D: Milestone Detection
|
|
685
|
-
|
|
684
|
+
### 7D: Milestone & Code Branch Detection
|
|
685
|
+
|
|
686
|
+
Derive both `CODE_BRANCH` and `MILESTONE_FLAG` from the reviewed PR before creating any findings. Unlike `commands/review-pr.md` (feature-lane PRs with short-lived HEAD branches that get deleted post-merge — see the rationale comment on that spec's finding template), `review-pr-staging.md` reviews long-lived branches (`staging`, `milestone/*`) that persist after merge, so `CODE_BRANCH` uses `headRefName` (the PR's HEAD — the branch actually being reviewed), consistent with forge#1391's original fix. This replaces the previous hardcoded `staging` literal, which was silently wrong whenever this spec reviewed a `milestone/X → staging` PR instead of a `staging → main` PR — `CODE_BRANCH` used to always say `staging` even when the reviewed branch was `milestone/X`.
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
CODE_BRANCH=$(gh pr view ${PR_NUMBER} --json headRefName --jq '.headRefName')
|
|
690
|
+
MILESTONE_FLAG=""
|
|
691
|
+
|
|
692
|
+
# scripts/derive-finding-milestone.sh is the single source of truth for
|
|
693
|
+
# milestone derivation — commands/review-pr.md and commands/review-pr-staging.md
|
|
694
|
+
# both call it identically so the two specs cannot independently drift.
|
|
695
|
+
# <!-- forge#2443 -->
|
|
696
|
+
MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R {GH_REPO})
|
|
697
|
+
# Quoted so multi-word milestone titles survive as a single value wherever
|
|
698
|
+
# MILESTONE_FLAG is later interpolated — matches the quoting convention already
|
|
699
|
+
# used for --title/--body-file/--label below.
|
|
700
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone \"$MILESTONE_TITLE\""
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Plain staging→main reviews resolve `CODE_BRANCH` to `staging` dynamically (same value as before, now derived rather than hardcoded) and typically resolve no milestone. A milestone→staging review resolves `CODE_BRANCH` to `milestone/X` and (via the shared script) the matching milestone.
|
|
686
704
|
|
|
687
705
|
### 7E: Deduplicate Against Existing Issues
|
|
688
706
|
Check for open review-finding issues at same file:line → skip. Closed issues at same location → potential regression (elevate priority).
|
|
689
707
|
|
|
690
708
|
### 7F: Create Issues
|
|
691
|
-
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:
|
|
709
|
+
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:P0-P3 (derived from Severity — see below). Body includes: source branch context (`${CODE_BRANCH}`, derived in Phase 7D — not a hardcoded `staging` literal), code context, evidence, validation checklist.
|
|
692
710
|
|
|
693
711
|
**For each finding** (that passes dedup), create issue through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") instead of calling the raw issue-creation command directly:
|
|
694
712
|
```bash
|
|
@@ -716,10 +734,10 @@ Files that need changes:
|
|
|
716
734
|
|
|
717
735
|
## Source Branch Context
|
|
718
736
|
|
|
719
|
-
**Code branch**: `
|
|
720
|
-
**Worktree base**: `origin/
|
|
737
|
+
**Code branch**: `[CODE_BRANCH]`
|
|
738
|
+
**Worktree base**: `origin/[CODE_BRANCH]`
|
|
721
739
|
|
|
722
|
-
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/
|
|
740
|
+
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[CODE_BRANCH]`
|
|
723
741
|
|
|
724
742
|
## Code Context
|
|
725
743
|
[10 lines around finding]
|
|
@@ -733,8 +751,32 @@ Files that need changes:
|
|
|
733
751
|
- [ ] If VALIDATED: fix implemented and tested on correct branch
|
|
734
752
|
ISSUE_EOF
|
|
735
753
|
|
|
754
|
+
# STAGING_FINDING_SEVERITY is extracted from the finding's own **Severity**
|
|
755
|
+
# body field (set above in the heredoc) — example assignment shown here for
|
|
756
|
+
# clarity, same convention as STAGING_FINDING_TITLE etc.
|
|
757
|
+
STAGING_FINDING_SEVERITY="LOW"
|
|
758
|
+
|
|
759
|
+
# priority:* label is a deterministic function of the finding's **Severity**
|
|
760
|
+
# (CRITICAL/HIGH/MEDIUM/LOW) — NEVER of its Confidence
|
|
761
|
+
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
762
|
+
# source of truth for this mapping; commands/review-pr.md calls the identical
|
|
763
|
+
# script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
764
|
+
STAGING_FINDING_PRIORITY=$(bash scripts/severity-to-priority.sh "$STAGING_FINDING_SEVERITY")
|
|
765
|
+
STAGING_FINDING_PRIORITY_EXIT=$?
|
|
766
|
+
|
|
767
|
+
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
768
|
+
# on a missing/unrecognized severity. Proceeding with an empty $STAGING_FINDING_PRIORITY
|
|
769
|
+
# would call Skill(issue, --label "") instead of aborting issue creation for this finding.
|
|
770
|
+
# Mirrors the identical check in commands/review-pr.md. <!-- forge#2479 -->
|
|
771
|
+
if [ "$STAGING_FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
772
|
+
echo "PRIORITY: severity-to-priority.sh failed (exit $STAGING_FINDING_PRIORITY_EXIT) for severity '$STAGING_FINDING_SEVERITY' — skipping finding issue creation"
|
|
773
|
+
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
774
|
+
else
|
|
775
|
+
|
|
736
776
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
737
|
-
|
|
777
|
+
# ${MILESTONE_FLAG} carries the Phase 7D derivation through — empty string is
|
|
778
|
+
# a no-op arg when the reviewed branch has no milestone (plain staging→main).
|
|
779
|
+
Skill(skill="issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}")
|
|
738
780
|
rm -f "$STAGING_FINDING_BODY_FILE"
|
|
739
781
|
|
|
740
782
|
# /issue has no machine-readable return contract — resolve the created issue's number by
|
|
@@ -746,9 +788,10 @@ for _resolve_attempt in 1 2 3; do
|
|
|
746
788
|
[ -n "$ISSUE_NUM" ] && break
|
|
747
789
|
sleep 2
|
|
748
790
|
done
|
|
791
|
+
fi
|
|
749
792
|
```
|
|
750
793
|
|
|
751
|
-
Labels: `review-finding` + `needs-validation` + `staging-review` + priority (`priority:P1`
|
|
794
|
+
Labels: `review-finding` + `needs-validation` + `staging-review` + priority. `priority:*` is derived from the finding's `**Severity**` field via `scripts/severity-to-priority.sh` (identical script used by `commands/review-pr.md` — single documented mapping, see that script's header comment): `CRITICAL` → `priority:P0`, `HIGH` → `priority:P1`, `MEDIUM` → `priority:P2`, `LOW` → `priority:P3`. **Never derive `priority:*` from Confidence** (CONFIRMED/LIKELY/POSSIBLE) — conflating the two axes previously mislabeled LOW-severity CONFIRMED findings as `priority:P1`, defeating orchestrate's P3 batching rule. <!-- forge#2447 -->
|
|
752
795
|
|
|
753
796
|
**No pre-filtering**: Every finding becomes an issue. Validation agents sort out false positives downstream.
|
|
754
797
|
|
package/commands/review-pr.md
CHANGED
|
@@ -1626,26 +1626,28 @@ gh label create "false-positive" --color "CCCCCC" --description "Review finding
|
|
|
1626
1626
|
|
|
1627
1627
|
**Milestone detection:**
|
|
1628
1628
|
```bash
|
|
1629
|
-
#
|
|
1630
|
-
#
|
|
1629
|
+
# BASE_BRANCH is what the finding template stamps as **Code branch** below
|
|
1630
|
+
# (see the rationale comment at that template block) — HEAD_BRANCH is no
|
|
1631
|
+
# longer needed here; derive-finding-milestone.sh resolves its own
|
|
1632
|
+
# base/head branch lookup internally for its Tier 3 slug match.
|
|
1631
1633
|
BASE_BRANCH=$(gh pr view ${PR_NUMBER} --json baseRefName --jq '.baseRefName')
|
|
1632
|
-
HEAD_BRANCH=$(gh pr view ${PR_NUMBER} --json headRefName --jq '.headRefName')
|
|
1633
1634
|
MILESTONE_FLAG=""
|
|
1634
1635
|
|
|
1635
|
-
#
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1636
|
+
# scripts/derive-finding-milestone.sh is the single source of truth for
|
|
1637
|
+
# milestone derivation — commands/review-pr.md and commands/review-pr-staging.md
|
|
1638
|
+
# both call it identically so the two specs cannot independently drift the
|
|
1639
|
+
# way they did before this fix (forge#2443 Instance B/C: findings from a
|
|
1640
|
+
# milestone-lane PR sometimes inherited the milestone and sometimes didn't,
|
|
1641
|
+
# depending on incidental inheritance rather than an explicit step). The
|
|
1642
|
+
# script's 3-tier resolution (PR's own milestone -> originating issue's
|
|
1643
|
+
# milestone via Closes/Fixes/Resolves #N -> branch-slug match against
|
|
1644
|
+
# milestone/* branches) supersedes the old base/head-branch-only slug match
|
|
1645
|
+
# that used to be hand-rolled here.
|
|
1646
|
+
MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R "${REPO}")
|
|
1647
|
+
# Quoted so multi-word milestone titles (e.g. "Watch & Fleet Observability") survive
|
|
1648
|
+
# as a single value wherever MILESTONE_FLAG is later interpolated — matches the
|
|
1649
|
+
# quoting convention already used for --title/--body-file/--label below.
|
|
1650
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone \"$MILESTONE_TITLE\""
|
|
1649
1651
|
```
|
|
1650
1652
|
|
|
1651
1653
|
**Dedup against existing issues (MANDATORY before creating):**
|
|
@@ -1763,10 +1765,26 @@ Files that need changes:
|
|
|
1763
1765
|
|
|
1764
1766
|
## Source Branch Context
|
|
1765
1767
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1768
|
+
<!--
|
|
1769
|
+
BASE_BRANCH, not HEAD_BRANCH, is correct here (forge#2443). This looks like
|
|
1770
|
+
a revert of forge#1391 ("review-finding template stamps BASE_BRANCH instead
|
|
1771
|
+
of HEAD_BRANCH — fixes target wrong code state") but is NOT: forge#1391's
|
|
1772
|
+
scenario was a staging->main review where HEAD (staging) is a long-lived
|
|
1773
|
+
branch that already had the bug's code, while BASE (main) had not yet
|
|
1774
|
+
received it. review-pr.md's scenario is different — HEAD here is a
|
|
1775
|
+
short-lived feat/*|fix/* branch that Phase 6E deletes once the auto-merge
|
|
1776
|
+
step completes, which happens SYNCHRONOUSLY within this same /review-pr run,
|
|
1777
|
+
before any downstream consumer (a human, or /work-on) ever reads this
|
|
1778
|
+
finding. By the time the finding is read, HEAD no longer exists but BASE
|
|
1779
|
+
does, and BASE now contains the merged code the finding is about.
|
|
1780
|
+
commands/review-pr-staging.md keeps HEAD_BRANCH semantics (as a dynamic
|
|
1781
|
+
${CODE_BRANCH}, not a hardcoded literal) because ITS reviewed branches
|
|
1782
|
+
(staging, milestone/*) are long-lived — consistent with forge#1391.
|
|
1783
|
+
-->
|
|
1784
|
+
**Code branch**: `[BASE_BRANCH]`
|
|
1785
|
+
**Worktree base**: `origin/[BASE_BRANCH]`
|
|
1786
|
+
|
|
1787
|
+
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[BASE_BRANCH]`
|
|
1770
1788
|
|
|
1771
1789
|
## Code Context
|
|
1772
1790
|
[10 lines around finding]
|
|
@@ -1785,8 +1803,30 @@ Files that need changes:
|
|
|
1785
1803
|
[BATCHABLE_ANNOTATION]
|
|
1786
1804
|
ISSUE_EOF
|
|
1787
1805
|
|
|
1806
|
+
# FINDING_SEVERITY is extracted from the finding's own **Severity** body field
|
|
1807
|
+
# (set above in the heredoc) — example assignment shown here for clarity, same
|
|
1808
|
+
# convention as FINDING_ISSUE_TITLE etc.
|
|
1809
|
+
FINDING_SEVERITY="LOW"
|
|
1810
|
+
|
|
1811
|
+
# priority:* label is a deterministic function of the finding's **Severity**
|
|
1812
|
+
# (CRITICAL/HIGH/MEDIUM/LOW) — NEVER of its Confidence
|
|
1813
|
+
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
1814
|
+
# source of truth for this mapping; commands/review-pr-staging.md calls the
|
|
1815
|
+
# identical script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
1816
|
+
FINDING_PRIORITY=$(bash scripts/severity-to-priority.sh "$FINDING_SEVERITY")
|
|
1817
|
+
FINDING_PRIORITY_EXIT=$?
|
|
1818
|
+
|
|
1819
|
+
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
1820
|
+
# on a missing/unrecognized severity, mirroring the DEDUP_EXIT idiom above. Proceeding
|
|
1821
|
+
# with an empty $FINDING_PRIORITY would call Skill(issue, --label "") instead of aborting
|
|
1822
|
+
# issue creation for this finding. <!-- forge#2479 -->
|
|
1823
|
+
if [ "$FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
1824
|
+
echo "PRIORITY: severity-to-priority.sh failed (exit $FINDING_PRIORITY_EXIT) for severity '$FINDING_SEVERITY' — skipping finding issue creation"
|
|
1825
|
+
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
1826
|
+
else
|
|
1827
|
+
|
|
1788
1828
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
1789
|
-
Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"
|
|
1829
|
+
Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG}")
|
|
1790
1830
|
rm -f "$FINDING_ISSUE_BODY_FILE"
|
|
1791
1831
|
|
|
1792
1832
|
# /issue has no machine-readable return contract (it's a user-facing command, not a work-on
|
|
@@ -1799,15 +1839,16 @@ for _resolve_attempt in 1 2 3; do
|
|
|
1799
1839
|
[ -n "$ISSUE_NUM" ] && break
|
|
1800
1840
|
sleep 2
|
|
1801
1841
|
done
|
|
1842
|
+
fi
|
|
1802
1843
|
```
|
|
1803
1844
|
|
|
1804
|
-
Labels: `review-finding` + `needs-validation` + priority (`priority:P1`
|
|
1845
|
+
Labels: `review-finding` + `needs-validation` + priority. `priority:*` is derived from the finding's `**Severity**` field via `scripts/severity-to-priority.sh` (single documented mapping — see that script's header comment): `CRITICAL` → `priority:P0`, `HIGH` → `priority:P1`, `MEDIUM` → `priority:P2`, `LOW` → `priority:P3`. **Never derive `priority:*` from Confidence** (CONFIRMED/LIKELY/POSSIBLE) — Confidence and Severity are independent axes; conflating them previously mislabeled LOW-severity CONFIRMED findings as `priority:P1`, defeating the P3 batching rule below. <!-- forge#2447 -->
|
|
1805
1846
|
|
|
1806
|
-
**P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (
|
|
1847
|
+
**P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (i.e. `**Severity**: LOW`), check whether the finding qualifies for batching. If the affected file does NOT touch a security or billing path, substitute `[BATCHABLE_ANNOTATION]` with the `<!-- FORGE:BATCHABLE -->` marker. This signals the orchestrator's Phase 1 batching rule that this finding can be grouped with other P3s in the same domain:
|
|
1807
1848
|
|
|
1808
1849
|
```bash
|
|
1809
1850
|
# Determine batchable eligibility for this finding
|
|
1810
|
-
|
|
1851
|
+
# $FINDING_PRIORITY was already computed above (severity-to-priority.sh) — reuse it, do not re-derive.
|
|
1811
1852
|
BATCHABLE_ANNOTATION=""
|
|
1812
1853
|
|
|
1813
1854
|
if [ "$FINDING_PRIORITY" = "priority:P3" ]; then
|
|
@@ -2089,19 +2130,41 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
|
|
|
2089
2130
|
# STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
|
|
2090
2131
|
else
|
|
2091
2132
|
|
|
2092
|
-
# Previously-escalated re-review guard <!-- Added: forge#1810 -->
|
|
2133
|
+
# Previously-escalated re-review guard <!-- Added: forge#1810; base-scoped: forge#2570 -->
|
|
2093
2134
|
# If the linked issue currently carries needs-human, this PR was escalated at some
|
|
2094
2135
|
# earlier point (VERDICT/purpose-regression/calibration/trust/mergeability) and has
|
|
2095
2136
|
# now been remediated + re-reviewed back to a clean, mergeable APPROVED state above.
|
|
2096
|
-
#
|
|
2097
|
-
# #
|
|
2098
|
-
#
|
|
2099
|
-
#
|
|
2137
|
+
#
|
|
2138
|
+
# forge#2570: the hold is now scoped by the PR's target branch. This PR still transitions
|
|
2139
|
+
# to workflow:awaiting-merge (clearing needs-human) in BOTH cases below — the transition is
|
|
2140
|
+
# unchanged; only who acts next differs:
|
|
2141
|
+
# - `main` / deploy gate (or an unresolved base — fail closed): held for a HUMAN merge
|
|
2142
|
+
# decision, because `staging → main` is the genuine human gate no agent performs.
|
|
2143
|
+
# - any other base (staging, milestone/* — reversible integration branches): NOT held for
|
|
2144
|
+
# a human. It is parked for remediate.md Phase M7's base-scoped auto-land bar (bot APPROVED
|
|
2145
|
+
# + quality-gate pass), which merges it on the SAME condition the normal /work-on fast lane
|
|
2146
|
+
# uses for the same target branch — no manual click. This reconciles the asymmetry where a
|
|
2147
|
+
# bot-only re-review (authorAssociation=NONE) could never satisfy the strict ≥2 verified-
|
|
2148
|
+
# human bar and so stranded staging PRs the fast lane would auto-merge.
|
|
2100
2149
|
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels \
|
|
2101
2150
|
--jq '[.labels[].name] | any(. == "needs-human")' 2>/dev/null || echo "false")
|
|
2151
|
+
GUARD_BASE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2102
2152
|
|
|
2103
2153
|
if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
2104
|
-
|
|
2154
|
+
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not
|
|
2155
|
+
# an empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"`
|
|
2156
|
+
# check so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
2157
|
+
if [ -n "$GUARD_BASE" ] && [ "$GUARD_BASE" != "null" ] && [ "$GUARD_BASE" != "main" ]; then
|
|
2158
|
+
# forge#2570: non-`main` base — defer to remediate.md M7's auto-land, not a human.
|
|
2159
|
+
# forge#2626: standalone `/review-pr` (no remediate.md M6→M7 chain) would otherwise
|
|
2160
|
+
# strand this PR at `workflow:awaiting-merge` with no actionable guidance — add a
|
|
2161
|
+
# manual-merge fallback so the comment is correct in both the chained and standalone cases.
|
|
2162
|
+
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
2163
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non-\`main\` base \`${GUARD_BASE}\`. It is parked at \`workflow:awaiting-merge\` for the remediation auto-land bar (\`remediate.md\` Phase M7, forge#2570) — which auto-merges it on the same fast-lane condition the normal \`/work-on\` path uses for this target branch — NOT for a human merge decision. If no remediation \`M7\` follows (e.g. this review was invoked standalone), merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
2164
|
+
else
|
|
2165
|
+
# forge#2570: `main` / deploy gate (or unresolved base — fail closed to the human gate).
|
|
2166
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check. This PR targets the deploy gate (\`${GUARD_BASE:-unresolved base}\`), so it is held at \`workflow:awaiting-merge\` for a human merge decision — \`staging → main\` is the genuine human gate. Merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
2167
|
+
fi
|
|
2105
2168
|
RESOLUTION=$(resolve_script 'transition-label')
|
|
2106
2169
|
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
2107
2170
|
case "$TIER" in
|
|
@@ -2111,7 +2174,9 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
2111
2174
|
--remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
2112
2175
|
;;
|
|
2113
2176
|
esac
|
|
2114
|
-
# STOP — do not attempt gh pr merge
|
|
2177
|
+
# STOP — do not attempt gh pr merge here. The merge decision belongs to a human for a
|
|
2178
|
+
# `main`/deploy-gate base, or to remediate.md Phase M7's base-scoped auto-land bar for a
|
|
2179
|
+
# non-`main` base (forge#2570). Either way this guard only parks at workflow:awaiting-merge.
|
|
2115
2180
|
else
|
|
2116
2181
|
# Checkpoint comment on issue
|
|
2117
2182
|
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
|
|
@@ -2229,13 +2294,30 @@ Post a welcome comment when all three conditions are true:
|
|
|
2229
2294
|
# Read config — default enabled when key is absent
|
|
2230
2295
|
WELCOME_ENABLED=$(yq '.community.welcome_new_contributors // true' forge.yaml 2>/dev/null || echo 'true')
|
|
2231
2296
|
|
|
2232
|
-
if [ "$IS_FIRST_TIME_CONTRIBUTOR" = "true" ] && [ "$WELCOME_ENABLED" != "false" ]; then
|
|
2297
|
+
if [ "$IS_FIRST_TIME_CONTRIBUTOR" = "true" ] && [ "$WELCOME_ENABLED" != "false" ] && [ "$REVIEW_MODE" = "single-pr" ]; then
|
|
2233
2298
|
# Read optional custom values from forge.yaml community section
|
|
2234
2299
|
CONTRIBUTING_URL=$(yq '.community.contributing_url // "CONTRIBUTING.md"' forge.yaml 2>/dev/null || echo 'CONTRIBUTING.md')
|
|
2235
2300
|
CUSTOM_CONVENTIONS=$(yq '.community.conventions // ""' forge.yaml 2>/dev/null || echo '')
|
|
2236
2301
|
PROJECT_NAME_WELCOME=$(yq '.project.name // "this project"' forge.yaml 2>/dev/null || echo 'this project')
|
|
2237
2302
|
|
|
2238
|
-
|
|
2303
|
+
# Resolve the conventions text into a plain shell variable BEFORE interpolating it into
|
|
2304
|
+
# the --body string below. A nested $(...) command substitution containing an escaped
|
|
2305
|
+
# \$VAR here is never expanded — see forge#2214.
|
|
2306
|
+
if [ -n "$CUSTOM_CONVENTIONS" ]; then
|
|
2307
|
+
CONVENTIONS_TEXT="$CUSTOM_CONVENTIONS"
|
|
2308
|
+
else
|
|
2309
|
+
CONVENTIONS_TEXT="- **Conventional commits**: prefix your commit messages with \`fix(scope):\`, \`feat(scope):\`, \`refactor(scope):\`, etc.
|
|
2310
|
+
- **DCO sign-off**: all commits must be signed off with \`git commit -s\` (Developer Certificate of Origin — required for the dual-license model).
|
|
2311
|
+
Fix unsigned commits: \`git commit --amend -s --no-edit && git push --force-with-lease\` <!-- allowlist:check-command-side-effects -->
|
|
2312
|
+
- **Issues, not inline fixes**: when you spot a bug or improvement opportunity, open a GitHub issue rather than fixing it inline — so it flows through the pipeline with full traceability."
|
|
2313
|
+
fi
|
|
2314
|
+
|
|
2315
|
+
# Unquoted heredoc (not the corpus's quoted <<'EOF' form) — this body needs live
|
|
2316
|
+
# ${VAR} interpolation, which a quoted heredoc would disable. Literal backticks in
|
|
2317
|
+
# the static text are still escaped (\`) since they remain live inside an unquoted
|
|
2318
|
+
# heredoc, same as inside a double-quoted string. See forge#2223.
|
|
2319
|
+
gh pr comment "$ARGUMENTS" --body "$(cat <<EOF
|
|
2320
|
+
## Welcome to ${PROJECT_NAME_WELCOME}! 🎉
|
|
2239
2321
|
|
|
2240
2322
|
Hi @${PR_AUTHOR} — this looks like your first merged contribution here. Thanks for opening a PR!
|
|
2241
2323
|
|
|
@@ -2245,17 +2327,16 @@ The automated review pipeline (/review-pr) ran against your PR. It spawned domai
|
|
|
2245
2327
|
|
|
2246
2328
|
### Key conventions
|
|
2247
2329
|
|
|
2248
|
-
$
|
|
2249
|
-
- **DCO sign-off**: all commits must be signed off with \`git commit -s\` (Developer Certificate of Origin — required for the dual-license model).
|
|
2250
|
-
Fix unsigned commits: \`git commit --amend -s --no-edit && git push --force-with-lease\`
|
|
2251
|
-
- **Issues, not inline fixes**: when you spot a bug or improvement opportunity, open a GitHub issue rather than fixing it inline — so it flows through the pipeline with full traceability.")
|
|
2330
|
+
${CONVENTIONS_TEXT}
|
|
2252
2331
|
|
|
2253
2332
|
### Getting started
|
|
2254
2333
|
|
|
2255
2334
|
- Contributing guide: [\`${CONTRIBUTING_URL}\`](${CONTRIBUTING_URL})
|
|
2256
2335
|
- Run \`npx forgedock\` to install the pipeline commands locally if you want to run /review-pr or /work-on yourself.
|
|
2257
2336
|
|
|
2258
|
-
Welcome aboard!
|
|
2337
|
+
Welcome aboard!
|
|
2338
|
+
EOF
|
|
2339
|
+
)"
|
|
2259
2340
|
fi
|
|
2260
2341
|
```
|
|
2261
2342
|
|
package/commands/upgrade-deps.md
CHANGED
|
@@ -376,7 +376,12 @@ Store created issue numbers as `CREATED_ISSUES` (empty when `DRY_RUN=true`).
|
|
|
376
376
|
For each issue in `CREATED_ISSUES`, invoke `/work-on` via the Skill tool:
|
|
377
377
|
|
|
378
378
|
```
|
|
379
|
-
Skill(
|
|
379
|
+
# DRY_RUN guard — must stay above the Skill() call it guards (forge#1609).
|
|
380
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
381
|
+
echo "[DRY RUN] Would invoke: Skill(skill: \"work-on\", args: \"{ISSUE_NUMBER}\")"
|
|
382
|
+
else
|
|
383
|
+
Skill(skill: "work-on", args: "{ISSUE_NUMBER}")
|
|
384
|
+
fi
|
|
380
385
|
```
|
|
381
386
|
|
|
382
387
|
Run **sequentially** — each `/work-on` invocation is heavyweight. If one fails, continue to the next and record the outcome.
|
|
@@ -389,7 +394,7 @@ FIX_RESULTS = [
|
|
|
389
394
|
]
|
|
390
395
|
```
|
|
391
396
|
|
|
392
|
-
**DRY_RUN check**:
|
|
397
|
+
**DRY_RUN check**: enforced inline above — the guard wraps the `Skill()` call rather than being stated after it, so a dry run cannot invoke `/work-on` even if only this section is read in isolation (forge#1609).
|
|
393
398
|
|
|
394
399
|
---
|
|
395
400
|
|
|
@@ -12,6 +12,8 @@ argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG]"
|
|
|
12
12
|
**Invoked by**: `work-on.md` routing loop, when `INVESTIGATE_RESULT.decompose = YES`.
|
|
13
13
|
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set labels. STOP — each sub-issue runs its own /work-on.
|
|
14
14
|
|
|
15
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are now registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on `DECOMPOSE:YES` instead of terminating in place, so a headless `runIssue()` walk now actually dispatches this subcommand rather than stopping short.
|
|
16
|
+
|
|
15
17
|
**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. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
|
|
16
18
|
**NEVER use plan mode (EnterPlanMode).**
|
|
17
19
|
|
|
@@ -263,8 +263,8 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, body: .bod
|
|
|
263
263
|
```
|
|
264
264
|
|
|
265
265
|
**Resume logic**:
|
|
266
|
-
- If `<!-- FORGE:INVESTIGATOR -->` comment exists AND `<!-- INVESTIGATION:COMPLETE -->` is present in the SAME comment → investigation already complete, EXIT (return existing verdict to caller)
|
|
267
|
-
- If `<!-- FORGE:INVESTIGATOR -->` comment exists BUT `<!-- INVESTIGATION:COMPLETE -->` is
|
|
266
|
+
- If `<!-- FORGE:INVESTIGATOR -->` comment exists AND (`<!-- INVESTIGATION:COMPLETE -->` OR `<!-- INVESTIGATION:INVALID -->`) is present in the SAME comment → investigation already complete, EXIT (return existing verdict to caller). `INVESTIGATION:INVALID` is the terminal sentinel Phase 1C emits for an INVALID verdict (see Phase 1C below) — it is just as much a completion marker as `INVESTIGATION:COMPLETE`, not an "interrupted" state.
|
|
267
|
+
- If `<!-- FORGE:INVESTIGATOR -->` comment exists BUT NEITHER `<!-- INVESTIGATION:COMPLETE -->` NOR `<!-- INVESTIGATION:INVALID -->` is present → investigation was interrupted, delete the partial comment and restart:
|
|
268
268
|
```bash
|
|
269
269
|
gh api repos/{GH_REPO}/issues/comments/{COMMENT_ID} -X DELETE
|
|
270
270
|
```
|
|
@@ -355,10 +355,25 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
355
355
|
|
|
356
356
|
## Phase 1C: Post Investigation Comment
|
|
357
357
|
|
|
358
|
-
The comment MUST include
|
|
358
|
+
The comment MUST include a terminal sentinel at the very end, AFTER all required sections are present. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
|
|
359
|
+
|
|
360
|
+
- **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase checks for it explicitly (ahead of `INVESTIGATION:COMPLETE`) and routes to `terminalReason: "invalid"`; `bin/hooks/interactive-engine.mjs`'s `PHASE_MARKERS` table also already treats it as terminal. Emitting `INVESTIGATION:COMPLETE` for an INVALID verdict is what previously caused every completed investigation to read as `{verdict: "CONFIRMED"}` regardless of actual outcome — do NOT regress this (forge#2350).
|
|
361
|
+
- **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` in Phase 1D — only INVALID gets the distinct terminal sentinel).
|
|
362
|
+
|
|
363
|
+
Compute the sentinel once, before building the comment body:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
if [ "{VERDICT}" = "INVALID" ]; then
|
|
367
|
+
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:INVALID -->"
|
|
368
|
+
else
|
|
369
|
+
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:COMPLETE -->"
|
|
370
|
+
fi
|
|
371
|
+
```
|
|
359
372
|
|
|
360
373
|
**CODEC PATH (forge#1727)**: Construct the annotation body via the protocol codec — do NOT hand-roll the `<!-- FORGE:INVESTIGATOR -->` header. Use `forge-annotation.sh write INVESTIGATOR --field ...` or `node packages/protocol/src/cli.js emit INVESTIGATOR --field ...` to produce the opening tag and completion sentinel. Fill in the Markdown body sections below. The full pattern:
|
|
361
374
|
|
|
375
|
+
**Caveat (forge#2368)**: `packages/protocol/src/types.js`'s `INVESTIGATOR.completionSentinel` is a single fixed value (`'INVESTIGATION:COMPLETE'`) and `packages/protocol/src/emit.js` appends it unconditionally — the codec does NOT currently support the verdict-conditional sentinel selection described above, and cannot emit `INVESTIGATION:INVALID`. Using the CODEC PATH for an INVALID-verdict investigation would silently regress the forge#2350 fix (every investigation would again read as `{verdict: "CONFIRMED"}`). Until the codec is extended to support a verdict-conditional sentinel, the hand-rolled block above/below (using the `${INVESTIGATION_SENTINEL}` variable computed above) is the authoritative path for Phase 1C — do not use the CODEC PATH for this annotation.
|
|
376
|
+
|
|
362
377
|
```bash
|
|
363
378
|
# Build the annotation body via codec (escaping and sentinel handled by codec)
|
|
364
379
|
ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
@@ -368,6 +383,7 @@ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
|
368
383
|
--field "Task Type={TASK_TYPE}" \
|
|
369
384
|
--field "Decomposition Assessment={YES|NO} — {reason}")
|
|
370
385
|
# ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
|
|
386
|
+
# NOTE: the codec's sentinel is fixed — do not use this path when Verdict=INVALID (see caveat above).
|
|
371
387
|
# Append the Markdown body sections to it before posting.
|
|
372
388
|
```
|
|
373
389
|
|
|
@@ -450,9 +466,11 @@ ACCEPTANCE_CHECK: id=ac-4 type=command target="grep -qE '(>= ?2|2\+)' commands/o
|
|
|
450
466
|
|
|
451
467
|
**Skipping**: if the issue has no verifiable acceptance criteria and none can be derived from the recommendation, emit a single sentinel: `ACCEPTANCE_CHECK: id=ac-skip type=skipped target="none" matcher="none" description=No machine-checkable criteria available — human review required`
|
|
452
468
|
${ANNOTATION_LINK_FOOTER}
|
|
453
|
-
|
|
469
|
+
${INVESTIGATION_SENTINEL}"
|
|
454
470
|
```
|
|
455
471
|
|
|
472
|
+
**Do not hardcode `<!-- INVESTIGATION:COMPLETE -->` as the closing line.** The closing line MUST be the `${INVESTIGATION_SENTINEL}` variable computed above — it resolves to `<!-- INVESTIGATION:INVALID -->` for an INVALID verdict and `<!-- INVESTIGATION:COMPLETE -->` otherwise. `INVESTIGATION:COMPLETE` and `INVESTIGATION:INVALID` are mutually exclusive within a single posted comment — never emit both.
|
|
473
|
+
|
|
456
474
|
---
|
|
457
475
|
|
|
458
476
|
## Phase 1C.5: Create Knowledge Gist
|
|
@@ -20,6 +20,8 @@ argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--
|
|
|
20
20
|
|
|
21
21
|
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` PR's own remediation. It does NOT implement the `needs-human` sub-label taxonomy (#1815's scope) and it does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810) — that guard's existing safe-default (`workflow:awaiting-merge` on any clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check *after* that guard has already fired.
|
|
22
22
|
|
|
23
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/remediate`) and completion marker (`FORGE:REMEDIATION:COMPLETE`, including the `**Re-gate outcome**` field Phase M8 posts below) are now registered in the headless engine's phase table — `RESERVED_TYPES.REMEDIATION` in `packages/protocol/src/types.js`, `remediate` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and a matching `remediate` entry in `bin/engine/phases.mjs`'s `PHASES` array (`entryCondition`: review committed + `terminalReason: "needs-human"`; `detectOutcome` maps each re-gate outcome to the right terminal reason). **Known limitation, documented in that engine entry's own comment**: a single continuous `runIssue()` walk cannot reach this phase today — `review`'s `"blocked"` outcome and the needs-human divergence guard both terminate the run before `remediate`'s `entryCondition` is ever evaluated. This subcommand therefore continues to run exactly as documented above — as its own separate top-level entry point — until a follow-up issue live-wires the engine to reconstruct state from GitHub for a dedicated remediation run.
|
|
24
|
+
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Inputs
|
|
@@ -253,11 +255,51 @@ REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
|
253
255
|
APPROVED_COUNT=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED_COUNT=${APPROVED_COUNT:-0}
|
|
254
256
|
```
|
|
255
257
|
|
|
256
|
-
**Auto-land bar** (
|
|
258
|
+
**Auto-land bar — base-branch scoped** (forge#2570): the condition the PR must clear to auto-land depends on its target branch. This reconciles the remediation bar with the normal `/work-on` fast-lane merge bar for the *same target branch*, while keeping the strict human-verified bar only where a human is genuinely in the loop (the `staging → main` deploy gate).
|
|
259
|
+
|
|
260
|
+
**Re-derive the base fresh — do NOT reuse `$PR_BASE` for this decision** (forge#2624): `$PR_BASE` was resolved in Phase M0 as `PR_BASE="${PR_BASE:-$(... .baseRefName)}"` — a caller-supplied `--base` wins over the PR's actual live base whenever non-empty. That is fine for M0's own purposes (worktree base, display text), but this is a security-relevant decision point: a wrong/stale `--base staging` on a PR that actually targets `main` must never be allowed to relax the strict deploy-gate bar. Mirror `review-pr.md`'s sibling guard (`GUARD_BASE`, which always re-fetches `baseRefName` fresh at its own decision point) by re-querying the PR's live base here, independent of whatever `$PR_BASE` currently holds:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# forge#2624: re-fetch baseRefName fresh from the PR itself — never trust the
|
|
264
|
+
# M0-resolved $PR_BASE for this decision, since M0 prefers a caller-supplied
|
|
265
|
+
# --base over the live value. This is the one line in this phase that makes
|
|
266
|
+
# a trust/security decision, so it must be immune to a caller-overridable input.
|
|
267
|
+
LIVE_BASE_REF=$(gh pr view {PR_NUMBER} {GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
268
|
+
|
|
269
|
+
# forge#2570: `main` (and any deploy-gate base) keeps the strict #1809 Q1 verified-human bar;
|
|
270
|
+
# every other base (staging, milestone/* — the reversible integration branches) reconciles to
|
|
271
|
+
# the fast-lane bar. Key on "is the deploy gate", NOT the literal string "staging", so milestone
|
|
272
|
+
# branches reconcile too. Fail closed: only a KNOWN, non-empty, non-`main` LIVE base reconciles
|
|
273
|
+
# to the fast lane; an empty/unresolved fetch is treated as the deploy gate (strict) so a base-
|
|
274
|
+
# resolution failure (or a caller passing a stale/incorrect --base) can never accidentally
|
|
275
|
+
# relax the bar.
|
|
276
|
+
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not an
|
|
277
|
+
# empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"` check
|
|
278
|
+
# so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
279
|
+
if [ -n "$LIVE_BASE_REF" ] && [ "$LIVE_BASE_REF" != "null" ] && [ "$LIVE_BASE_REF" != "main" ]; then IS_DEPLOY_GATE=false; else IS_DEPLOY_GATE=true; fi
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Non-`main` base (`IS_DEPLOY_GATE=false` — staging / milestone)** — reconcile to the fast-lane bar. `review-pr.md`'s Phase 8 guard only parks a PR at `workflow:awaiting-merge` after a clean, mergeable `APPROVED` re-review (the same bot-`APPROVED` signal the normal fast lane auto-merges on), so the only additional condition is this remediation's own quality gate:
|
|
283
|
+
1. `GATE_PASSED = true` from this remediation's Phase M3 quality-gate loop.
|
|
284
|
+
|
|
285
|
+
The strict `APPROVED_COUNT >= 2` verified-human requirement does NOT apply here: it is structurally unsatisfiable for bot-only pipeline review (bot reviews are `authorAssociation=NONE`), and `staging` is reversible — the real human gate is `staging → main`, which no agent performs. This makes the remediation bar identical to the normal fast-lane bar for the same target branch (the issue's core ask). The `authorAssociation` trust filter above is **unchanged** — the relaxation is scoped by *target branch only*, never by *who* may approve (forge#1976/#2519).
|
|
286
|
+
|
|
287
|
+
**`main` base (`IS_DEPLOY_GATE=true` — deploy gate)** — keep the strict #1809 Q1 bar, BOTH conditions required:
|
|
257
288
|
1. `APPROVED_COUNT >= 2` — at least two distinct adversarial `APPROVED:` review comments from repo collaborators (`OWNER`/`MEMBER`/`COLLABORATOR` authorAssociation only — see trust filter above; same counting convention as `work-on.md` Phase 7A).
|
|
258
289
|
2. `GATE_PASSED = true` from this remediation's own Phase M3 quality-gate loop.
|
|
259
290
|
|
|
260
|
-
**
|
|
291
|
+
**Evaluate the base-scoped bar**:
|
|
292
|
+
```bash
|
|
293
|
+
if [ "$IS_DEPLOY_GATE" = "true" ]; then
|
|
294
|
+
# main / deploy gate — strict verified-human bar
|
|
295
|
+
{ [ "${APPROVED_COUNT:-0}" -ge 2 ] && [ "${GATE_PASSED:-false}" = "true" ]; } && BAR_MET=true || BAR_MET=false
|
|
296
|
+
else
|
|
297
|
+
# staging / milestone — fast-lane bar (bot APPROVED already implied by workflow:awaiting-merge)
|
|
298
|
+
[ "${GATE_PASSED:-false}" = "true" ] && BAR_MET=true || BAR_MET=false
|
|
299
|
+
fi
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**If the bar is met** (`BAR_MET=true`):
|
|
261
303
|
```bash
|
|
262
304
|
gh pr merge {PR_NUMBER} {GH_FLAG} --merge
|
|
263
305
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state --jq '.state')
|
|
@@ -279,7 +321,7 @@ else
|
|
|
279
321
|
fi
|
|
280
322
|
```
|
|
281
323
|
|
|
282
|
-
**If the bar is NOT met
|
|
324
|
+
**If the bar is NOT met** (`BAR_MET=false`): leave the issue at `workflow:awaiting-merge` exactly as `review-pr.md`'s guard set it — do NOT attempt a merge. `RE_GATE_OUTCOME="HELD-AWAITING-MERGE"`. Fail-safe direction: any doubt about the bar (including an unresolved or unexpected `LIVE_BASE_REF`, which leaves `IS_DEPLOY_GATE=true`-equivalent strict handling) defaults to holding, matching `review-pr.md`'s own existing default for every other caller.
|
|
283
325
|
|
|
284
326
|
---
|
|
285
327
|
|
|
@@ -289,7 +331,16 @@ Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this
|
|
|
289
331
|
|
|
290
332
|
```bash
|
|
291
333
|
case "$RE_GATE_OUTCOME" in
|
|
292
|
-
AUTO-LANDED)
|
|
334
|
+
AUTO-LANDED)
|
|
335
|
+
# forge#2570: the bar that was met differs by target branch. Non-`main` (staging/milestone)
|
|
336
|
+
# lands on the fast-lane bar (clean re-review APPROVED + quality-gate pass), identical to the
|
|
337
|
+
# normal /work-on path; `main`/deploy-gate lands only on the strict ≥2 verified-human bar.
|
|
338
|
+
if [ "${IS_DEPLOY_GATE:-true}" = "false" ]; then
|
|
339
|
+
AUTO_LAND_BAR_TEXT="MET — fast-lane bar for non-\`main\` base (clean re-review APPROVED + quality gate pass), matching the normal /work-on merge bar for the same target branch"
|
|
340
|
+
else
|
|
341
|
+
AUTO_LAND_BAR_TEXT="MET (${APPROVED_COUNT:-0} APPROVED: reviews + quality gate pass)"
|
|
342
|
+
fi
|
|
343
|
+
OUTCOME_DETAIL="to {PR_BASE}" ;;
|
|
293
344
|
HELD-AWAITING-MERGE) AUTO_LAND_BAR_TEXT="NOT MET (${APPROVED_COUNT:-0} APPROVED: reviews)"; OUTCOME_DETAIL="at workflow:awaiting-merge" ;;
|
|
294
345
|
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
295
346
|
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|