forgedock 1.10.3 → 1.11.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/ci-audit.md +3 -3
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-pr.md +6 -6
- package/commands/explain.md +2 -2
- package/commands/fix-ci.md +2 -2
- package/commands/forge-stats.md +1 -1
- package/commands/forgedock-init.md +3 -3
- package/commands/optimize.md +5 -5
- package/commands/orchestrate/phase-1-resolve.md +2 -2
- package/commands/orchestrate/phase-4-execution.md +182 -41
- package/commands/quality-gate.md +1 -1
- package/commands/recover-orphans.md +6 -6
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-staging.md +28 -6
- package/commands/review-pr.md +434 -120
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +2 -2
- package/commands/test-gate.md +7 -7
- package/commands/work-on/build/architect.md +21 -0
- package/commands/work-on/build/context.md +1 -1
- package/commands/work-on/build/implement.md +17 -0
- package/commands/work-on/build/validate.md +2 -2
- package/commands/work-on/build.md +98 -9
- package/commands/work-on/close.md +33 -11
- package/commands/work-on/decompose.md +19 -5
- package/commands/work-on/investigate.md +34 -4
- package/commands/work-on/remediate.md +30 -6
- package/commands/work-on/review.md +10 -8
- package/commands/work-on.md +20 -11
- package/docs/CONFIG.md +30 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/docs/WORK-ON-RUNTIME.md +22 -2
- package/docs/site/architecture.md +1 -1
- package/docs/spec/forge-protocol-v1.md +10 -0
- package/docs/spec/review-protocol.md +1 -0
- package/package.json +1 -1
- package/packages/protocol/src/types.js +17 -0
- package/scripts/check-spec-bash.sh +45 -2
- package/scripts/check-spec-bash.test.sh +24 -1
- package/scripts/check-spec-markers.sh +53 -0
- package/scripts/diff-size.sh +84 -0
- package/scripts/diff-size.test.sh +117 -0
- package/scripts/forge-root.test.sh +15 -0
- package/scripts/remediation-marker-match.test.sh +51 -0
- package/scripts/review-pr-arguments.test.sh +180 -0
- package/scripts/trusted-comments.test.sh +23 -0
package/commands/ci-audit.md
CHANGED
|
@@ -253,7 +253,7 @@ For each gap detected (tool present in repo but no CI validation step found), fi
|
|
|
253
253
|
**Skip issue creation if `--dry-run` flag is passed** (or `forge.yaml → ci_audit.dry_run: true`):
|
|
254
254
|
```bash
|
|
255
255
|
DRY_RUN=$(yq '.ci_audit.dry_run // "false"' forge.yaml 2>/dev/null || echo "false")
|
|
256
|
-
[ "$1" = "--dry-run" ] || [ "$2" = "--dry-run" ] && DRY_RUN="true"
|
|
256
|
+
[ "${1}" = "--dry-run" ] || [ "${2}" = "--dry-run" ] && DRY_RUN="true"
|
|
257
257
|
```
|
|
258
258
|
|
|
259
259
|
### 3A: Determine validator recommendation
|
|
@@ -262,8 +262,8 @@ For each gap, compose a concrete, version-aware recommendation:
|
|
|
262
262
|
|
|
263
263
|
```bash
|
|
264
264
|
get_validator_recommendation() {
|
|
265
|
-
local tool="$1"
|
|
266
|
-
local version="$2" # may be empty if unknown
|
|
265
|
+
local tool="${1}"
|
|
266
|
+
local version="${2}" # may be empty if unknown
|
|
267
267
|
|
|
268
268
|
case "$tool" in
|
|
269
269
|
traefik)
|
package/commands/compat-audit.md
CHANGED
|
@@ -28,7 +28,7 @@ fi
|
|
|
28
28
|
# Fallback: resolve from this command file's own location if yq is unavailable
|
|
29
29
|
# or forge.yaml is absent. The commands/ directory is always inside FORGE_HOME.
|
|
30
30
|
if [ -z "$FORGE_HOME_PATH" ] || [ ! -d "$FORGE_HOME_PATH" ]; then
|
|
31
|
-
FORGE_HOME_PATH="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)"
|
|
31
|
+
FORGE_HOME_PATH="$(cd "$(dirname "${BASH_SOURCE[0]:-${0}}")/.." 2>/dev/null && pwd)"
|
|
32
32
|
fi
|
|
33
33
|
export FORGE_HOME_PATH
|
|
34
34
|
```
|
package/commands/deploy-pr.md
CHANGED
|
@@ -93,12 +93,12 @@ DRY_RUN=false
|
|
|
93
93
|
# Parse optional flags
|
|
94
94
|
shift 2>/dev/null || true
|
|
95
95
|
while [[ $# -gt 0 ]]; do
|
|
96
|
-
case "$1" in
|
|
97
|
-
--target) TARGET="$2"; shift 2 ;;
|
|
98
|
-
--issue) ISSUE_NUMBER="$2"; shift 2 ;;
|
|
99
|
-
--repo) GH_REPO="$2"; GH_FLAG="-R $GH_REPO"; shift 2 ;;
|
|
100
|
-
--max-ci-iterations) MAX_CI_ITER="$2"; shift 2 ;;
|
|
101
|
-
--max-review-iterations) MAX_REVIEW_ITER="$2"; shift 2 ;;
|
|
96
|
+
case "${1}" in
|
|
97
|
+
--target) TARGET="${2}"; shift 2 ;;
|
|
98
|
+
--issue) ISSUE_NUMBER="${2}"; shift 2 ;;
|
|
99
|
+
--repo) GH_REPO="${2}"; GH_FLAG="-R $GH_REPO"; shift 2 ;;
|
|
100
|
+
--max-ci-iterations) MAX_CI_ITER="${2}"; shift 2 ;;
|
|
101
|
+
--max-review-iterations) MAX_REVIEW_ITER="${2}"; shift 2 ;;
|
|
102
102
|
--dry-run) DRY_RUN=true; shift ;;
|
|
103
103
|
*) shift ;;
|
|
104
104
|
esac
|
package/commands/explain.md
CHANGED
|
@@ -140,10 +140,10 @@ Extract each annotation block from the comments. Each `<!-- FORGE:TYPE -->` bloc
|
|
|
140
140
|
```bash
|
|
141
141
|
# Helper: extract comment body containing a FORGE marker
|
|
142
142
|
extract_forge() {
|
|
143
|
-
local marker="$1"
|
|
143
|
+
local marker="${1}"
|
|
144
144
|
echo "$ALL_COMMENTS" | awk "
|
|
145
145
|
/<!-- ${marker}(:[A-Z]*)? -->/{found=1; buffer=\"\"}
|
|
146
|
-
found {buffer = buffer \$0 ORS}
|
|
146
|
+
found {buffer = buffer \$(0) ORS}
|
|
147
147
|
/<!-- ${marker}[^>]*:COMPLETE -->/{if(found){print buffer; found=0; buffer=\"\"}}
|
|
148
148
|
END {if(found && length(buffer)>0) print buffer}
|
|
149
149
|
"
|
package/commands/fix-ci.md
CHANGED
|
@@ -49,7 +49,7 @@ Parse `$ARGUMENTS`:
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
# Extract PR number (accept raw number or full GitHub URL)
|
|
52
|
-
PR_INPUT=$(echo "$ARGUMENTS" | awk '{print $1}')
|
|
52
|
+
PR_INPUT=$(echo "$ARGUMENTS" | awk '{print $(1)}')
|
|
53
53
|
if echo "$PR_INPUT" | grep -qE '^https?://'; then
|
|
54
54
|
PR_NUMBER=$(echo "$PR_INPUT" | grep -oE '[0-9]+$')
|
|
55
55
|
else
|
|
@@ -58,7 +58,7 @@ fi
|
|
|
58
58
|
|
|
59
59
|
MAX_ATTEMPTS=5
|
|
60
60
|
if echo "$ARGUMENTS" | grep -q -- "--max-attempts"; then
|
|
61
|
-
MAX_ATTEMPTS=$(echo "$ARGUMENTS" | grep -oE -- '--max-attempts [0-9]+' | awk '{print $2}')
|
|
61
|
+
MAX_ATTEMPTS=$(echo "$ARGUMENTS" | grep -oE -- '--max-attempts [0-9]+' | awk '{print $(2)}')
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
64
|
if [ -z "$PR_NUMBER" ]; then
|
package/commands/forge-stats.md
CHANGED
|
@@ -104,7 +104,7 @@ for cmd in commands/*.md; do
|
|
|
104
104
|
done
|
|
105
105
|
|
|
106
106
|
echo ""
|
|
107
|
-
echo "Total: $(wc -l commands/*.md | tail -1 | awk '{print $1}') lines across $(ls commands/*.md | wc -l) commands"
|
|
107
|
+
echo "Total: $(wc -l commands/*.md | tail -1 | awk '{print $(1)}') lines across $(ls commands/*.md | wc -l) commands"
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
**After running, flag any command where:**
|
|
@@ -576,7 +576,7 @@ gh repo view "${OWNER}/${REPO}" --json name,url 2>/dev/null \
|
|
|
576
576
|
### 6B: Path existence
|
|
577
577
|
|
|
578
578
|
```bash
|
|
579
|
-
[ -d "$(grep 'root:' "$FORGE_YAML" | head -1 | awk '{print $2}' | tr -d '"')" ] \
|
|
579
|
+
[ -d "$(grep 'root:' "$FORGE_YAML" | head -1 | awk '{print $(2)}' | tr -d '"')" ] \
|
|
580
580
|
&& echo "✓ paths.root exists" \
|
|
581
581
|
|| echo "WARNING: paths.root does not exist on this machine"
|
|
582
582
|
```
|
|
@@ -588,7 +588,7 @@ gh repo view "${OWNER}/${REPO}" --json name,url 2>/dev/null \
|
|
|
588
588
|
```bash
|
|
589
589
|
# Only run if project_board section was written
|
|
590
590
|
if grep -q "^project_board:" "$FORGE_YAML"; then
|
|
591
|
-
PROJECT_ID=$(grep "project_id:" "$FORGE_YAML" | head -1 | awk '{print $2}' | tr -d '"')
|
|
591
|
+
PROJECT_ID=$(grep "project_id:" "$FORGE_YAML" | head -1 | awk '{print $(2)}' | tr -d '"')
|
|
592
592
|
# Validate PVT_ prefix before GraphQL call — any other format will produce a confusing API error
|
|
593
593
|
if [[ "$PROJECT_ID" != PVT_* ]]; then
|
|
594
594
|
echo "ERROR: project_board.project_id must start with 'PVT_' (got: '$PROJECT_ID')"
|
|
@@ -610,7 +610,7 @@ fi
|
|
|
610
610
|
if grep -q "^repos:" "$FORGE_YAML"; then
|
|
611
611
|
# Extract satellite repo names and verify each is accessible
|
|
612
612
|
grep -A 20 "^repos:" "$FORGE_YAML" | grep "repo:" | tail -n +2 | while read -r line; do
|
|
613
|
-
SAT_REPO=$(echo "$line" | awk '{print $2}' | tr -d '"')
|
|
613
|
+
SAT_REPO=$(echo "$line" | awk '{print $(2)}' | tr -d '"')
|
|
614
614
|
gh repo view "$SAT_REPO" --json name 2>/dev/null \
|
|
615
615
|
&& echo "✓ satellite repo ${SAT_REPO} accessible" \
|
|
616
616
|
|| echo "WARNING: satellite repo ${SAT_REPO} not accessible — check prefix and repo name"
|
package/commands/optimize.md
CHANGED
|
@@ -250,7 +250,7 @@ declare -A LABEL_MAP=(
|
|
|
250
250
|
)
|
|
251
251
|
|
|
252
252
|
map_label() {
|
|
253
|
-
local canonical="$1"
|
|
253
|
+
local canonical="${1}"
|
|
254
254
|
echo "${LABEL_MAP[$canonical]:-$canonical}"
|
|
255
255
|
}
|
|
256
256
|
SCRIPT_EOF
|
|
@@ -514,8 +514,8 @@ for candidate in "${TIER_A_PROMOTIONS[@]}"; do
|
|
|
514
514
|
# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
515
515
|
#
|
|
516
516
|
# CONTRACT:
|
|
517
|
-
# \$1 = absolute path to unified diff (git diff HEAD)
|
|
518
|
-
# \$2 = absolute path to worktree root
|
|
517
|
+
# \${1} = absolute path to unified diff (git diff HEAD)
|
|
518
|
+
# \${2} = absolute path to worktree root
|
|
519
519
|
# stdout = findings lines: SLUG | SEVERITY | FILE | MESSAGE
|
|
520
520
|
# exit 0 = pass, exit 1 = findings, exit 2 = inapplicable
|
|
521
521
|
# 30s timeout enforced by quality-gate (fail-closed)
|
|
@@ -612,8 +612,8 @@ ${CITATIONS_BODY}
|
|
|
612
612
|
## gate.d contract
|
|
613
613
|
|
|
614
614
|
Each script receives:
|
|
615
|
-
- \$1 = path to unified diff
|
|
616
|
-
- \$2 = path to worktree root
|
|
615
|
+
- \${1} = path to unified diff
|
|
616
|
+
- \${2} = path to worktree root
|
|
617
617
|
- exits 0 (pass), 1 (findings on stdout), or 2 (inapplicable)
|
|
618
618
|
- 30-second timeout enforced by quality-gate (fail-closed on timeout)
|
|
619
619
|
|
|
@@ -216,7 +216,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
216
216
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
217
217
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
218
218
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
219
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
219
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
220
220
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
221
221
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
222
222
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -406,7 +406,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
406
406
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
407
407
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
408
408
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
409
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
409
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
410
410
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
411
411
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
412
412
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -804,7 +804,7 @@ _l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || r
|
|
|
804
804
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
805
805
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
806
806
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
807
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
807
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
808
808
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
809
809
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
810
810
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -1178,7 +1178,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
1178
1178
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
1179
1179
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
1180
1180
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
1181
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
1181
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
1182
1182
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
1183
1183
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
1184
1184
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -1624,7 +1624,7 @@ classify_predecessor_state() {
|
|
|
1624
1624
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
1625
1625
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
1626
1626
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
1627
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
1627
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
1628
1628
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
1629
1629
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
1630
1630
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -2019,7 +2019,7 @@ done
|
|
|
2019
2019
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
2020
2020
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
2021
2021
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
2022
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2022
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2023
2023
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
2024
2024
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
2025
2025
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -2292,8 +2292,17 @@ done
|
|
|
2292
2292
|
# Idempotency guard: only one remediation attempt per PR, ever (single-attempt
|
|
2293
2293
|
# semantics — remediate.md's own Phase M0 enforces this too, but checking here
|
|
2294
2294
|
# avoids spawning a redundant agent that would immediately no-op on entry).
|
|
2295
|
-
|
|
2296
|
-
|
|
2295
|
+
# Leading-marker + trusted-author count (forge#3412): a comment that merely quotes FORGE:REMEDIATION
|
|
2296
|
+
# (review findings, INPR_FIX work order) must not suppress dispatch. Unreadable => skip dispatch (fail closed).
|
|
2297
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2298
|
+
TRUSTED_SCRIPT=""
|
|
2299
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
2300
|
+
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2301
|
+
done
|
|
2302
|
+
ALREADY_REMEDIATED=1
|
|
2303
|
+
if [ -n "$TRUSTED_SCRIPT" ] && GATING_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/${GATING_PR}/comments 2>/dev/null); then
|
|
2304
|
+
ALREADY_REMEDIATED=$(printf '%s' "$GATING_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:REMEDIATION -->' 2>/dev/null || echo "1")
|
|
2305
|
+
fi
|
|
2297
2306
|
|
|
2298
2307
|
if [ "$ALREADY_REMEDIATED" -eq 0 ]; then
|
|
2299
2308
|
echo "Dispatching remediation for #{PRED}'s gating PR #{GATING_PR} (needs-human)"
|
|
@@ -2321,66 +2330,198 @@ Do not ask the user questions — you are running autonomously in the background
|
|
|
2321
2330
|
|
|
2322
2331
|
**Re-review handoff (forge#3240)**: a remediation worker can finish with `REMEDIATE_RESULT: status: REREVIEW_REQUIRED` — the fix is pushed and CI is green, but the remediation had no sub-agent dispatch tool, so `review-pr` (which refuses to review inline) never ran. The issue is then at `workflow:in-review` WITHOUT `needs-human`, so the trigger above does not fire and the `ALREADY_REMEDIATED` guard (which counts any `FORGE:REMEDIATION`) is irrelevant. This is not a human gate, but it must never be left for a poll to find: it ends in a re-review from THIS session (which has dispatch) or in the terminal fallback below. On the next completion-monitoring cycle, for the issue that just completed, run the three steps in order. <!-- Updated: forge#3406 -->
|
|
2323
2332
|
|
|
2324
|
-
**Step 1 — detect (bash; read-only, no dispatch, no marker write).** A marker counts only from a trusted author, using the same predicate as `trail_escalation_state` above (`OWNER`, the orchestrator login from `resolve_orch_login`, or a `MEMBER`/`COLLABORATOR` with admin/maintain/write permission; a failed permission lookup drops the row), and only as the comment's leading marker, never as text quoted inside another comment (forge#3412). The
|
|
2333
|
+
**Step 1 — detect (bash; read-only, no dispatch, no marker write).** One authoritative per-head marker (forge#3413): `<!-- FORGE:REREVIEW_DISPATCHED -->` as the comment's FIRST line, then `Head: <sha>`, `Actor: orchestrator|router`, `Claim: <actor>-<epoch>-<pid>` and `Lease: <UTC ISO-8601>`. It is a **claim** posted and read back BEFORE `review-pr` is dispatched, by whichever actor dispatches (this orchestrator in Step 2, or the worker router in `commands/work-on.md` Phase 0A.1), so either actor sees the other's claim. A marker counts only from a trusted author, using the same predicate as `trail_escalation_state` above (`OWNER`, the orchestrator login from `resolve_orch_login`, or a `MEMBER`/`COLLABORATOR` with admin/maintain/write permission; a failed permission lookup drops the row), and only as the comment's leading marker, never as text quoted inside another comment (forge#3412). The `REREVIEW-REQUIRED` remediation trail must embed the CURRENT head SHA (`**Head**:` line written by `work-on/remediate.md` Phase M8), so a trail for an older head never triggers a re-review of a newer one. The issue's own labels are consulted first: only `workflow:in-review` without `needs-human`, still open, is eligible. A lookup error, or an unresolved orchestrator login, is `UNREADABLE`, never "no marker" (fail closed, no dispatch):
|
|
2325
2334
|
|
|
2326
2335
|
```bash
|
|
2336
|
+
# SHARED HELPERS (forge#3411): Step 2's claim/read-back re-uses these verbatim — one predicate, no copy to drift.
|
|
2337
|
+
# The set is `rereview_trusted_rows` + the `REREVIEW_*` definitions below AND its transitive deps `resolve_orch_login` and
|
|
2338
|
+
# `trail_cache_dir` (defined earlier in this file, NOT in this block). A standalone caller (the work-on router) must paste those too.
|
|
2339
|
+
# Trusted rows only, same predicate as trail_escalation_state (forge#3207): OWNER, the orchestrator login, or
|
|
2340
|
+
# MEMBER/COLLABORATOR with admin/maintain/write permission. A failed permission lookup drops the row (fail closed).
|
|
2341
|
+
# stdin: rows of {id, body, login, assoc, at}; stdout: the trusted rows.
|
|
2342
|
+
rereview_trusted_rows() {
|
|
2343
|
+
local ORCH PERMS ROW RA RL RP
|
|
2344
|
+
ORCH=$(resolve_orch_login); PERMS=""
|
|
2345
|
+
while IFS= read -r ROW; do
|
|
2346
|
+
[ -n "$ROW" ] || continue
|
|
2347
|
+
RA=$(printf '%s' "$ROW" | jq -r '.assoc'); RL=$(printf '%s' "$ROW" | jq -r '.login')
|
|
2348
|
+
if [ "$RA" = "OWNER" ] || { [ -n "$ORCH" ] && [ "$(printf '%s' "$RL" | tr '[:upper:]' '[:lower:]')" = "$(printf '%s' "$ORCH" | tr '[:upper:]' '[:lower:]')" ]; }; then
|
|
2349
|
+
printf '%s\n' "$ROW"; continue
|
|
2350
|
+
fi
|
|
2351
|
+
case "$RA" in MEMBER|COLLABORATOR) ;; *) continue ;; esac
|
|
2352
|
+
RP=$(printf '%s\n' "$PERMS" | awk -v l="$RL" '$1==l{print $2; exit}')
|
|
2353
|
+
if [ -z "$RP" ]; then RP=$(gh api "repos/{GH_REPO}/collaborators/${RL}/permission" --jq '.permission' 2>/dev/null || true); [ -n "$RP" ] || RP="none"; PERMS="${PERMS}${RL} ${RP}"$'\n'; fi
|
|
2354
|
+
case "$RP" in admin|maintain|write) printf '%s\n' "$ROW" ;; esac
|
|
2355
|
+
done < <(jq -c '.' 2>/dev/null)
|
|
2356
|
+
}
|
|
2357
|
+
# Per-head claim rows (forge#3412/#3413): leading marker AND the exact `Head: <sha>` line, sorted by comment id (forge#3429: an explicit total order, never implicit API row order), so `.[0]` is the lowest-id claim.
|
|
2358
|
+
REREVIEW_CLAIMS='[.[] | select((.body | startswith("<!-- FORGE:REREVIEW_DISPATCHED -->")) and (.body | contains("Head: " + $sha)))] | sort_by(.id)'
|
|
2359
|
+
REREVIEW_DONE_FILTER="${REREVIEW_CLAIMS} | length"
|
|
2360
|
+
# Lease (forge#3428): a claim older than REREVIEW_LEASE_SECS is NOT stale by age alone; it is stale only when there is also no
|
|
2361
|
+
# liveness (renewal, recent review activity, running checks). REREVIEW_MAX_SECS is the absolute ceiling for inferred liveness.
|
|
2362
|
+
REREVIEW_LEASE_SECS=3600
|
|
2363
|
+
REREVIEW_MAX_SECS=$(( REREVIEW_LEASE_SECS * 4 ))
|
|
2364
|
+
# Lease age reads the NEWEST row carrying the same `Claim:` id as the earliest claim (.[0]); ownership/LOST stays on .[0].
|
|
2365
|
+
REREVIEW_LEASE_FILTER='(.[0].body | capture("Claim: (?<c>[^\\n]+)").c) as $c | [.[] | select(.body | split("\n") | any(. == ("Claim: " + $c)))] | .[-1].body | capture("Lease: (?<t>[0-9T:Z-]+)").t'
|
|
2366
|
+
REREVIEW_FIRST_LEASE_FILTER='.[0].body | capture("Lease: (?<t>[0-9T:Z-]+)").t'
|
|
2367
|
+
# Explicit release: a trusted leading `<!-- FORGE:REREVIEW_RELEASED -->` row for this head (posted by the claimant after review-pr returned).
|
|
2368
|
+
REREVIEW_RELEASED_FILTER='[.[] | select((.body | startswith("<!-- FORGE:REREVIEW_RELEASED -->")) and (.body | contains("Head: " + $sha)))] | length'
|
|
2369
|
+
|
|
2327
2370
|
PRED="$NUM"
|
|
2328
2371
|
REREVIEW_STATE="NONE"; REREVIEW_PR=""; REREVIEW_SHA=""; REREVIEW_BASE=""
|
|
2329
|
-
|
|
2372
|
+
# 1. Labels first (forge#3413 CONC-1): only an open issue at workflow:in-review WITHOUT needs-human is eligible.
|
|
2373
|
+
if ! ISSUE_META=$(gh issue view "$PRED" -R {GH_REPO} --json labels,state \
|
|
2374
|
+
--jq '"\(.state) \([.labels[].name] | map(select(. == "workflow:in-review" or . == "needs-human")) | sort | join(","))"' 2>/dev/null); then
|
|
2375
|
+
REREVIEW_STATE="UNREADABLE"
|
|
2376
|
+
elif [ "${ISSUE_META%% *}" != "OPEN" ] || [ "${ISSUE_META#* }" != "workflow:in-review" ]; then
|
|
2377
|
+
REREVIEW_STATE="NONE" # closed/merged, already at needs-human, or not at workflow:in-review: nothing to re-review
|
|
2378
|
+
elif [ -z "$(resolve_orch_login)" ]; then
|
|
2379
|
+
# SEC-8: with no own login the orchestrator's own marker is dropped as untrusted, so a dispatch could never be
|
|
2380
|
+
# recorded or deduped. Fail closed BEFORE any dispatch.
|
|
2381
|
+
REREVIEW_STATE="UNREADABLE"
|
|
2382
|
+
elif ! REREVIEW_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
|
|
2330
2383
|
--json number,body --jq '[.[] | select(.body | test("(?i)closes\\s+#'"${PRED}"'\\b"))][0].number // empty' 2>/dev/null); then
|
|
2331
2384
|
REREVIEW_STATE="UNREADABLE"
|
|
2332
2385
|
elif [ -n "$REREVIEW_PR" ]; then
|
|
2333
2386
|
if ! PR_META=$(gh pr view "$REREVIEW_PR" -R {GH_REPO} --json headRefOid,baseRefName --jq '"\(.headRefOid) \(.baseRefName)"' 2>/dev/null) \
|
|
2334
2387
|
|| ! RAW=$(gh api --paginate "repos/{GH_REPO}/issues/${REREVIEW_PR}/comments" \
|
|
2335
|
-
--jq '.[] | {body: (.body // ""), login: (.user.login // ""), assoc: (.author_association // "")}' 2>/dev/null); then
|
|
2388
|
+
--jq '.[] | {id: (.id // 0), body: (.body // ""), login: (.user.login // ""), assoc: (.author_association // ""), at: (.created_at // "")}' 2>/dev/null); then
|
|
2336
2389
|
REREVIEW_STATE="UNREADABLE"
|
|
2337
2390
|
else
|
|
2338
2391
|
REREVIEW_SHA="${PR_META%% *}"; REREVIEW_BASE="${PR_META#* }"
|
|
2339
|
-
|
|
2340
|
-
#
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2392
|
+
TRUSTED_RAW=$(printf '%s\n' "$RAW" | rereview_trusted_rows)
|
|
2393
|
+
# Leading-marker match only (forge#3412), scoped to the CURRENT head (forge#3413 CONC-2): the M8 trail comment
|
|
2394
|
+
# carries `**Head**: <sha>`. NEEDS_ANY counts every trail row (any head) to detect a stale, pre-#3413 trail.
|
|
2395
|
+
NEEDS_REREVIEW=$(printf '%s' "$TRUSTED_RAW" | jq -s --arg sha "$REREVIEW_SHA" '[.[] | select((.body | startswith("<!-- FORGE:REMEDIATION -->")) and (.body | contains("<!-- FORGE:REMEDIATION:COMPLETE -->")) and (.body | contains("REREVIEW-REQUIRED")) and (.body | contains($sha)))] | length' 2>/dev/null) || NEEDS_REREVIEW=""
|
|
2396
|
+
NEEDS_ANY=$(printf '%s' "$TRUSTED_RAW" | jq -s '[.[] | select((.body | startswith("<!-- FORGE:REMEDIATION -->")) and (.body | contains("<!-- FORGE:REMEDIATION:COMPLETE -->")) and (.body | contains("REREVIEW-REQUIRED")))] | length' 2>/dev/null) || NEEDS_ANY=""
|
|
2397
|
+
REREVIEW_DONE=$(printf '%s' "$TRUSTED_RAW" | jq -s --arg sha "$REREVIEW_SHA" "$REREVIEW_DONE_FILTER" 2>/dev/null) || REREVIEW_DONE=""
|
|
2398
|
+
if [ -z "$NEEDS_REREVIEW" ] || [ -z "$NEEDS_ANY" ] || [ -z "$REREVIEW_DONE" ]; then REREVIEW_STATE="UNREADABLE"
|
|
2399
|
+
elif [ "$NEEDS_REREVIEW" -ge 1 ] && [ "$REREVIEW_DONE" -eq 0 ]; then
|
|
2400
|
+
REREVIEW_STATE="PENDING" # DRY_RUN is handled in Step 2 (no `DRY_RUN != true` term here, so its branch is reachable)
|
|
2401
|
+
elif [ "$NEEDS_REREVIEW" -ge 1 ]; then
|
|
2402
|
+
# A claim for this head already exists (orchestrator or router). forge#3428: age alone never makes it stale.
|
|
2403
|
+
# released -> NONE (claimant finished; nothing to dispatch or escalate)
|
|
2404
|
+
# lease still fresh -> CLAIMED (the lease is the newest row of the owner's claim; a renewal extends it)
|
|
2405
|
+
# lease expired -> LIVENESS probe: PR not open -> NONE; recent trusted FORGE:REVIEW* activity inside the lease
|
|
2406
|
+
# window or running checks -> CLAIMED; no sign of life -> STALE. A lookup error is UNREADABLE.
|
|
2407
|
+
# past REREVIEW_MAX_SECS since the first claim -> inferred liveness no longer counts (a hung claimant is escalated).
|
|
2408
|
+
RELEASED=$(printf '%s' "$TRUSTED_RAW" | jq -s --arg sha "$REREVIEW_SHA" "$REREVIEW_RELEASED_FILTER" 2>/dev/null) || RELEASED=""
|
|
2409
|
+
CLAIM_T=$(printf '%s' "$TRUSTED_RAW" | jq -s -r --arg sha "$REREVIEW_SHA" "${REREVIEW_CLAIMS} | ${REREVIEW_LEASE_FILTER}" 2>/dev/null || true)
|
|
2410
|
+
FIRST_T=$(printf '%s' "$TRUSTED_RAW" | jq -s -r --arg sha "$REREVIEW_SHA" "${REREVIEW_CLAIMS} | ${REREVIEW_FIRST_LEASE_FILTER}" 2>/dev/null || true)
|
|
2411
|
+
CLAIM_EPOCH=$(printf '%s' "$CLAIM_T" | jq -R 'fromdateiso8601' 2>/dev/null || true)
|
|
2412
|
+
FIRST_EPOCH=$(printf '%s' "$FIRST_T" | jq -R 'fromdateiso8601' 2>/dev/null || true)
|
|
2413
|
+
NOW=$(date -u +%s)
|
|
2414
|
+
if [ -z "$RELEASED" ]; then REREVIEW_STATE="UNREADABLE"
|
|
2415
|
+
elif [ "$RELEASED" -ge 1 ]; then REREVIEW_STATE="NONE"
|
|
2416
|
+
elif [ -z "$CLAIM_EPOCH" ] || [ -z "$FIRST_EPOCH" ]; then REREVIEW_STATE="STALE" # malformed claim (no parseable Lease)
|
|
2417
|
+
elif [ $(( NOW - CLAIM_EPOCH )) -le "$REREVIEW_LEASE_SECS" ]; then REREVIEW_STATE="CLAIMED"
|
|
2418
|
+
elif ! LIVE_META=$(gh pr view "$REREVIEW_PR" -R {GH_REPO} --json state,statusCheckRollup \
|
|
2419
|
+
--jq '"\(.state) \([.statusCheckRollup[]? | select(((.status // "") | test("^(IN_PROGRESS|QUEUED|PENDING|WAITING)$")) or ((.state // "") == "PENDING"))] | length)"' 2>/dev/null) \
|
|
2420
|
+
|| ! ACTIVE=$(printf '%s' "$TRUSTED_RAW" | jq -s --argjson cutoff "$(( NOW - REREVIEW_LEASE_SECS ))" \
|
|
2421
|
+
'[.[] | select((.body | startswith("<!-- FORGE:REVIEW")) and (((.at | fromdateiso8601?) // 0) > $cutoff))] | length' 2>/dev/null); then
|
|
2422
|
+
REREVIEW_STATE="UNREADABLE" # liveness lookup failed: fail closed, never STALE
|
|
2423
|
+
else
|
|
2424
|
+
LIVE_PR_STATE="${LIVE_META%% *}"; LIVE_CHECKS="${LIVE_META#* }"
|
|
2425
|
+
case "$LIVE_CHECKS$ACTIVE" in *[!0-9]*|"") REREVIEW_STATE="UNREADABLE" ;;
|
|
2426
|
+
*) if [ "$LIVE_PR_STATE" != "OPEN" ]; then REREVIEW_STATE="NONE" # merged/closed: nothing to escalate
|
|
2427
|
+
elif [ $(( NOW - FIRST_EPOCH )) -gt "$REREVIEW_MAX_SECS" ]; then REREVIEW_STATE="STALE"
|
|
2428
|
+
elif [ "$LIVE_CHECKS" -gt 0 ] || [ "$ACTIVE" -gt 0 ]; then REREVIEW_STATE="CLAIMED" # liveness: claimant still working
|
|
2429
|
+
else REREVIEW_STATE="STALE"; fi ;;
|
|
2430
|
+
esac
|
|
2347
2431
|
fi
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
if [ -z "$RP" ]; then RP=$(gh api "repos/{GH_REPO}/collaborators/${RL}/permission" --jq '.permission' 2>/dev/null || true); [ -n "$RP" ] || RP="none"; PERMS="${PERMS}${RL} ${RP}"$'\n'; fi
|
|
2351
|
-
case "$RP" in admin|maintain|write) TRUSTED_RAW="${TRUSTED_RAW}${ROW}"$'\n' ;; esac
|
|
2352
|
-
done < <(printf '%s\n' "$RAW" | jq -c '.' 2>/dev/null)
|
|
2353
|
-
# Leading-marker match only (forge#3412): remediation trail comments start with <!-- FORGE:REMEDIATION -->.
|
|
2354
|
-
NEEDS_REREVIEW=$(printf '%s' "$TRUSTED_RAW" | jq -s '[.[] | select((.body | startswith("<!-- FORGE:REMEDIATION -->")) and (.body | contains("<!-- FORGE:REMEDIATION:COMPLETE -->")) and (.body | contains("REREVIEW-REQUIRED")))] | length' 2>/dev/null) || NEEDS_REREVIEW=""
|
|
2355
|
-
REREVIEW_DONE=$(printf '%s' "$TRUSTED_RAW" | jq -s --arg sha "$REREVIEW_SHA" '[.[] | select((.body | startswith("<!-- FORGE:REREVIEW_DISPATCHED -->")) and (.body | contains($sha)))] | length' 2>/dev/null) || REREVIEW_DONE=""
|
|
2356
|
-
if [ -z "$NEEDS_REREVIEW" ] || [ -z "$REREVIEW_DONE" ]; then REREVIEW_STATE="UNREADABLE"
|
|
2357
|
-
elif [ "$NEEDS_REREVIEW" -ge 1 ] && [ "$REREVIEW_DONE" -eq 0 ] && [ "${DRY_RUN:-false}" != "true" ]; then REREVIEW_STATE="PENDING"
|
|
2432
|
+
elif [ "$NEEDS_ANY" -ge 1 ]; then
|
|
2433
|
+
REREVIEW_STATE="STALE" # trail is for another head (or predates the Head line): never re-dispatch silently
|
|
2358
2434
|
fi
|
|
2359
2435
|
fi
|
|
2360
2436
|
fi
|
|
2361
|
-
|
|
2437
|
+
# forge#3429: also print the lowest-id (winning) claim's `Claim:` id so a re-check can tell its own claim from another actor's.
|
|
2438
|
+
FIRST_CLAIM_ID=""
|
|
2439
|
+
if [ -n "${TRUSTED_RAW:-}" ]; then FIRST_CLAIM_ID=$(printf '%s' "$TRUSTED_RAW" | jq -s -r --arg sha "$REREVIEW_SHA" "${REREVIEW_CLAIMS} | (.[0].body // \"\") | capture(\"Claim: (?<c>[^\\\\n]+)\").c // \"\"" 2>/dev/null || true); fi
|
|
2440
|
+
echo "REREVIEW_STATE=$REREVIEW_STATE PR=$REREVIEW_PR SHA=$REREVIEW_SHA BASE=$REREVIEW_BASE FIRST_CLAIM_ID=$FIRST_CLAIM_ID"
|
|
2362
2441
|
```
|
|
2363
2442
|
|
|
2364
|
-
`REREVIEW_STATE=NONE` means nothing to do. `UNREADABLE` fails closed: classify the issue `GATED` this cycle without writing anything, and re-run Step 1 next cycle. Only `PENDING` continues.
|
|
2443
|
+
`REREVIEW_STATE=NONE` means nothing to do (including an issue already at `needs-human`, closed, or not at `workflow:in-review`). `CLAIMED` means another actor holds a live claim for this head (an unexpired or renewed lease, or an expired lease with recent liveness such as a trusted `FORGE:REVIEW*` comment inside the lease window or running checks): do nothing, re-run Step 1 next cycle. A `FORGE:REREVIEW_RELEASED` marker for the head (posted by the claimant after `review-pr` returned) or a merged/closed PR is `NONE`: the claimant finished, so there is nothing to dispatch or escalate. `UNREADABLE` fails closed: classify the issue `GATED` this cycle without writing anything, and re-run Step 1 next cycle. `STALE` (an expired lease with no liveness and the PR still open, a lease past the `REREVIEW_MAX_SECS` ceiling, an unparseable claim, or a trail that does not match the current head) goes straight to the Step 3 terminal fallback with the reason `stale re-review claim or trail`, without dispatching. Only `PENDING` continues.
|
|
2444
|
+
|
|
2445
|
+
**Step 2 — dispatch (claim first, forge#3413).** The claim is written and read back BEFORE `review-pr` is invoked. A failed post or read-back means NO dispatch (fail closed, terminal fallback), so there is never a wasted duplicate dispatch, and a second actor sees the claim and stands down. Bash blocks are independent executions: run the claim block with the **SHARED HELPERS** (`rereview_trusted_rows`, `REREVIEW_CLAIMS`, `REREVIEW_DONE_FILTER`, the lease/released filters and `REREVIEW_LEASE_SECS` / `REREVIEW_MAX_SECS`, plus its transitive deps `resolve_orch_login` and `trail_cache_dir`, pasted if not already defined in the block) from Step 1 pasted verbatim at its top, and `REREVIEW_PR` / `REREVIEW_SHA` set to the values Step 1 printed. The read-back lists the PR's comments by number and applies the same trusted predicate. The EARLIEST trusted claim for the head wins, defined as the LOWEST comment id (`.[0]` of the id-sorted `REREVIEW_CLAIMS`, unchanged by renewals; forge#3429): if the first matching row does not carry this actor's `Claim:` id, another actor claimed first (`LOST`) and this one stands down. The read-back waits a short settle delay before the first read and re-reads once after `VERIFIED` (a lower-id claim that appears late demotes it to `LOST`). Any API failure, zero matching rows, or a failed confirming re-read is `UNVERIFIED` (fail closed, never `VERIFIED`). The block has no `exit`, so it is safe in a shared or sourced shell:
|
|
2446
|
+
|
|
2447
|
+
```bash
|
|
2448
|
+
REREVIEW_MARKER="UNVERIFIED"
|
|
2449
|
+
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
2450
|
+
echo "[DRY_RUN] would claim FORGE:REREVIEW_DISPATCHED for head ${REREVIEW_SHA} on PR #${REREVIEW_PR} and dispatch review-pr --auto-merge"
|
|
2451
|
+
REREVIEW_MARKER="DRYRUN"
|
|
2452
|
+
else
|
|
2453
|
+
CLAIM_ID="orchestrator-$(date -u +%s)-$$"
|
|
2454
|
+
post_marker() { gh pr comment "$REREVIEW_PR" -R {GH_REPO} --body "<!-- FORGE:REREVIEW_DISPATCHED -->
|
|
2455
|
+
Head: ${REREVIEW_SHA}
|
|
2456
|
+
Actor: orchestrator
|
|
2457
|
+
Claim: ${CLAIM_ID}
|
|
2458
|
+
Lease: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
2459
|
+
|
|
2460
|
+
Re-review claimed by the orchestrator: remediation had no sub-agent dispatch tool."; }
|
|
2461
|
+
# Retry the post once, and only when the first attempt failed (a duplicate with the same Claim id is harmless).
|
|
2462
|
+
post_marker || { sleep 2; post_marker || echo "REREVIEW claim post failed twice" >&2; }
|
|
2463
|
+
# Read back: the lowest-id trusted claim for this head must be ours (forge#3429). Settle delay before the first read so a
|
|
2464
|
+
# winner's comment that is still replicating is visible; one re-read after a short wait absorbs lag on failure.
|
|
2465
|
+
# rb_first_claim prints the body of the lowest-id claim; any failure or zero rows returns non-zero (fail closed).
|
|
2466
|
+
rb_first_claim() {
|
|
2467
|
+
local RB_RAW RB_TRUSTED RB_FIRST
|
|
2468
|
+
RB_RAW=$(gh api --paginate "repos/{GH_REPO}/issues/${REREVIEW_PR}/comments" \
|
|
2469
|
+
--jq '.[] | {id: (.id // 0), body: (.body // ""), login: (.user.login // ""), assoc: (.author_association // "")}' 2>/dev/null) || return 1
|
|
2470
|
+
RB_TRUSTED=$(printf '%s\n' "$RB_RAW" | rereview_trusted_rows) || return 1
|
|
2471
|
+
RB_FIRST=$(printf '%s' "$RB_TRUSTED" | jq -s -r --arg sha "$REREVIEW_SHA" "${REREVIEW_CLAIMS} | .[0].body // \"\"" 2>/dev/null) || return 1
|
|
2472
|
+
[ -n "$RB_FIRST" ] || return 1
|
|
2473
|
+
printf '%s' "$RB_FIRST"
|
|
2474
|
+
}
|
|
2475
|
+
sleep 2
|
|
2476
|
+
for _ATTEMPT in 1 2; do
|
|
2477
|
+
if RB_FIRST=$(rb_first_claim); then
|
|
2478
|
+
if printf '%s' "$RB_FIRST" | grep -qxF "Claim: ${CLAIM_ID}"; then REREVIEW_MARKER="VERIFIED"; else REREVIEW_MARKER="LOST"; fi
|
|
2479
|
+
break
|
|
2480
|
+
fi
|
|
2481
|
+
sleep 3
|
|
2482
|
+
done
|
|
2483
|
+
# Confirming re-read after VERIFIED: a lower-id claim that became visible late means LOST; a failed re-read means
|
|
2484
|
+
# UNVERIFIED (never dispatch on an unconfirmed claim).
|
|
2485
|
+
if [ "$REREVIEW_MARKER" = "VERIFIED" ]; then
|
|
2486
|
+
sleep 2
|
|
2487
|
+
if RB_CONFIRM=$(rb_first_claim); then
|
|
2488
|
+
printf '%s' "$RB_CONFIRM" | grep -qxF "Claim: ${CLAIM_ID}" || REREVIEW_MARKER="LOST"
|
|
2489
|
+
else
|
|
2490
|
+
REREVIEW_MARKER="UNVERIFIED"
|
|
2491
|
+
fi
|
|
2492
|
+
fi
|
|
2493
|
+
fi
|
|
2494
|
+
echo "REREVIEW_MARKER=$REREVIEW_MARKER"
|
|
2495
|
+
```
|
|
2365
2496
|
|
|
2366
|
-
|
|
2497
|
+
Then, on `REREVIEW_MARKER=VERIFIED` ONLY, dispatch with the `REREVIEW_PR`, `REREVIEW_BASE` and `PRED` values Step 1 printed (a real call, not part of the bash block above). `DRYRUN` records "Would re-review PR #${REREVIEW_PR} with review-pr --auto-merge; skipped (dry-run)." and dispatches nothing; `LOST` and `UNVERIFIED` dispatch nothing:
|
|
2367
2498
|
|
|
2368
2499
|
```
|
|
2369
2500
|
if DRY_RUN=true:
|
|
2370
2501
|
record "Would re-review PR #${REREVIEW_PR} with review-pr --auto-merge; skipped (dry-run)."
|
|
2371
|
-
|
|
2502
|
+
elif REREVIEW_MARKER == VERIFIED:
|
|
2372
2503
|
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${REREVIEW_PR} --auto-merge --issue ${PRED} --base ${REREVIEW_BASE} --gh-flag -R {GH_REPO}")
|
|
2373
2504
|
```
|
|
2374
2505
|
|
|
2375
|
-
If the call returns no `REVIEW_RESULT` (interrupted or no result), invoke it once more; this retry is the only one (bounded, independent of the Phase 4R `github-unavailable` waits)
|
|
2506
|
+
If the call returns no `REVIEW_RESULT` (interrupted or no result), invoke it once more; this retry is the only one (bounded, independent of the Phase 4R `github-unavailable` waits) and does not post a second claim: the claim for this head is already recorded.
|
|
2507
|
+
|
|
2508
|
+
**Renew and release (forge#3428).** After `review-pr` returns (any result, including none), the claimant posts a checked, DRY_RUN-guarded release so the next Step 1 reads `NONE` instead of aging the claim into `STALE`. Run it with the SHARED HELPERS (the same set, including `resolve_orch_login` and `trail_cache_dir`) pasted at the top and `REREVIEW_PR` / `REREVIEW_SHA` / `CLAIM_ID` from Step 2. A failed post is reported (never ignored) but is not fatal: liveness in Step 1 still protects a live claimant. Renewal is best-effort: between review phases the claimant may re-post `<!-- FORGE:REREVIEW_DISPATCHED -->` with the SAME `Claim:` id and a fresh `Lease:` (Step 1 reads the newest row of the earliest claim's id for lease age; the earliest-claim-wins read-back stays on `.[0]`):
|
|
2376
2509
|
|
|
2377
2510
|
```bash
|
|
2378
|
-
[ "${DRY_RUN:-false}" = "true" ]
|
|
2379
|
-
|
|
2380
|
-
|
|
2511
|
+
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
2512
|
+
echo "[DRY_RUN] would post FORGE:REREVIEW_RELEASED for head ${REREVIEW_SHA} on PR #${REREVIEW_PR}"
|
|
2513
|
+
else
|
|
2514
|
+
post_release() { gh pr comment "$REREVIEW_PR" -R {GH_REPO} --body "<!-- FORGE:REREVIEW_RELEASED -->
|
|
2515
|
+
Head: ${REREVIEW_SHA}
|
|
2516
|
+
Actor: orchestrator
|
|
2517
|
+
Claim: ${CLAIM_ID}
|
|
2518
|
+
|
|
2519
|
+
Re-review finished by the orchestrator: claim released."; }
|
|
2520
|
+
post_release || { sleep 2; post_release || echo "REREVIEW release post failed twice (liveness still protects the claim)" >&2; }
|
|
2521
|
+
fi
|
|
2381
2522
|
```
|
|
2382
2523
|
|
|
2383
|
-
**Step 3 — outcome.** On `REVIEW_RESULT: status: COMPLETE` (merged), drive `work-on:close` and let item 6.6 wake dependents as normal. If `review-pr` returns another result with its own labelled terminal state (CHANGES REQUESTED, or `needs-human` for genuine blockers), that verdict applies and the issue is `GATED` or failed per the existing rules. If dispatch failed, the skill did not resolve,
|
|
2524
|
+
**Step 3 — outcome.** On `REVIEW_RESULT: status: COMPLETE` (merged), drive `work-on:close` and let item 6.6 wake dependents as normal. If `review-pr` returns another result with its own labelled terminal state (CHANGES REQUESTED, or `needs-human` for genuine blockers), that verdict applies and the issue is `GATED` or failed per the existing rules. `REREVIEW_MARKER=LOST` (another actor claimed this head first) and `REREVIEW_STATE=CLAIMED` take no action: leave the issue as it is and re-check next cycle. Before applying the terminal fallback for `STALE`, re-read the PR state and the issue labels once more: a merged or closed PR, or an issue that already advanced, means skip the fallback and report the state (forge#3428). If dispatch failed, the skill did not resolve, the issue is still at `workflow:in-review` with no `needs-human` after the retry, Step 1 printed `STALE`, or Step 2 printed `REREVIEW_MARKER=UNVERIFIED` and the re-check below found no live claim, apply the **terminal fallback** defined in `commands/work-on.md` Phase 0A.1 (labels re-read first, comment with the reason, add `needs-human` and remove `workflow:in-review` in one edit, verify the write), classify the issue `GATED`, and surface it to the operator in the run summary as `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): <reason>`. For `REREVIEW_MARKER=UNVERIFIED`, first re-check for another actor's fresh claim (forge#3429): re-run the Step 1 CLAIMED/STALE/DONE classification for this head (with the SHARED HELPERS pasted at the top of that block). Map the result, reading the printed `FIRST_CLAIM_ID` (the lowest-id, winning claim, the same `.[0]` row that decides ownership everywhere else): if `FIRST_CLAIM_ID` equals this actor's own `CLAIM_ID`, the winning claim is our own unverified one, so go to the fallback (do not stand down on it). If `FIRST_CLAIM_ID` is another actor's id, the winning claim is not ours: classify it with the mapping below, exactly as the router's Claim pre-check step 4 does for `LOST` (forge#3431), so a `STALE` foreign winner (a crashed earlier claimant) goes to the fallback and never strands the issue, even though our own row still exists. Otherwise (and for a foreign winner) `CLAIMED` (another actor's live claim) and `NONE` (released or done) stand down like `LOST` (no dispatch, no fallback, re-check next cycle); `UNREADABLE` writes nothing (fail closed, classify `GATED` this cycle without the fallback, as in Step 1); `STALE` and `PENDING` go to the fallback. The reason is `re-review claim could not be recorded` and the report line is `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): marker write failed`; because the claim is written first, `UNVERIFIED` now means `review-pr` was NOT dispatched. Do not leave the issue `PENDING`. A failed label write is reported as such, not as done. The verified per-head claim bounds this to one re-review per PR head across the orchestrator and the router (no loop). Never review inline.
|
|
2384
2525
|
|
|
2385
2526
|
This satisfies #1809 Q2 (the orchestrator auto-dispatches remediation against the gated issue itself — the exact gap forge#1812's item 6.5/6.6 left open, since those items only ever track and wake *dependents*, never the gated PR's own remediation) — and closes forge#2243 (the gap that #1812's fix left open for *leaf* issues with no dependents: because this item's `$PRED` binding was never made explicit and self-contained, it was only ever reached while walking a dependent's predecessor list, so a `needs-human` issue that has no dependents never got remediation dispatched and its CHANGES-REQUESTED PR re-reviewed the same unchanged commit forever). The remediation agent's outcome is picked up on the **next** completion-monitoring cycle of this same Step 4B loop: if it lands (`workflow:merged`), item 6.6 below fires normally and wakes any `blocked-on-human-merge` dependents (if any exist — a leaf issue simply has none to wake); if it holds/re-escalates, the issue simply remains `GATED` and item 6.5 continues tracking its dependents (if any) unchanged.
|
|
2386
2527
|
|
|
@@ -2618,7 +2759,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
2618
2759
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
2619
2760
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
2620
2761
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
2621
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2762
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2622
2763
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
2623
2764
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
2624
2765
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -2656,7 +2797,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
2656
2797
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
2657
2798
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
2658
2799
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
2659
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2800
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
2660
2801
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
2661
2802
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
2662
2803
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -2907,7 +3048,7 @@ _cands="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_H
|
|
|
2907
3048
|
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2908
3049
|
[ -n "$_cfg" ] || continue
|
|
2909
3050
|
_cands="$_cands
|
|
2910
|
-
$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$0}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
3051
|
+
$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
2911
3052
|
done
|
|
2912
3053
|
_cands="$_cands
|
|
2913
3054
|
$PWD"
|
|
@@ -3266,7 +3407,7 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
3266
3407
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
3267
3408
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
3268
3409
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
3269
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
3410
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
3270
3411
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
3271
3412
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
3272
3413
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -3432,7 +3573,7 @@ for FINDING_NUM in "${BATCHING_CANDIDATES[@]}"; do
|
|
|
3432
3573
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
3433
3574
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
3434
3575
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
3435
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
3576
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
3436
3577
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
3437
3578
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
3438
3579
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
package/commands/quality-gate.md
CHANGED
|
@@ -1714,7 +1714,7 @@ if [ -f "$CLASSIFIER" ]; then
|
|
|
1714
1714
|
--retries 3 \
|
|
1715
1715
|
2>&1)
|
|
1716
1716
|
echo "$RESULT"
|
|
1717
|
-
CLASSIFICATION=$(echo "$RESULT" | grep '^CLASSIFICATION:' | awk '{print $2}')
|
|
1717
|
+
CLASSIFICATION=$(echo "$RESULT" | grep '^CLASSIFICATION:' | awk '{print $(2)}')
|
|
1718
1718
|
else
|
|
1719
1719
|
# Script not present — fall back to treating all test failures as real.
|
|
1720
1720
|
CLASSIFICATION="REAL"
|