forgedock 1.12.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +295 -78
- package/bin/engine/runlog.mjs +13 -2
- package/bin/engine-cli.mjs +1 -0
- package/bin/engine.mjs +8 -3
- package/bin/hooks/interactive-engine.mjs +3 -0
- package/bin/labels.json +5 -0
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +24 -2
- package/bin/watch-utils.mjs +1 -0
- package/commands/cleanup.md +1 -1
- package/commands/orchestrate/phase-4-execution.md +32 -17
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents.md +11 -11
- package/commands/review-pr.md +155 -24
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build.md +11 -4
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +22 -12
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +13 -13
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/forge-root.test.sh +35 -14
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/transition-label.sh +3 -1
package/commands/review-pr.md
CHANGED
|
@@ -1172,11 +1172,47 @@ echo "=== BASELINE ROSTER (top domains): $SELECTED_AGENTS ==="
|
|
|
1172
1172
|
| Cross-critical domains | `SCORE_AUTH >= 2` AND `SCORE_DATABASE >= 3` | Auth + Database |
|
|
1173
1173
|
| Cross-critical domains | `SCORE_BILLING >= 2` AND `SCORE_CONCURRENCY >= 2` | Billing + Concurrency + Database |
|
|
1174
1174
|
|
|
1175
|
+
**Roster label to marker domain (single source of truth)**: roster labels come in three spellings (score keys such as `INFRA`/`SCRAPING`, display names such as `Infrastructure`/`Scraping`, persona files such as `infra`/`scraper`). The marker domain in `<!-- FORGE:REVIEW-AGENT:{domain} -->` is always the canonical value below, never the lowercased roster label. Phase 4 stays fail closed on that canonical marker: no alias is accepted. <!-- Added: forge#3485 -->
|
|
1176
|
+
|
|
1177
|
+
| Roster label (any case) | Marker domain | Persona file |
|
|
1178
|
+
|-------------------------|---------------|--------------|
|
|
1179
|
+
| `Security` | `security` | `security.md` |
|
|
1180
|
+
| `Auth` | `auth` | `auth.md` |
|
|
1181
|
+
| `Billing` | `billing` | `billing.md` |
|
|
1182
|
+
| `Concurrency` | `concurrency` | `concurrency.md` |
|
|
1183
|
+
| `Database` | `database` | `database.md` |
|
|
1184
|
+
| `INFRA`, `Infrastructure` | `infra` | `infra.md` |
|
|
1185
|
+
| `SCRAPING`, `Scraping`, `Scraper` | `scraper` | `scraper.md` |
|
|
1186
|
+
| `FRONTEND`, `Frontend`, `Web` | `frontend` | `frontend.md` |
|
|
1187
|
+
| `API` | `api` | `api.md` |
|
|
1188
|
+
|
|
1189
|
+
A label outside the table falls back to its lowercase form. The `agent_domain()` helper is defined inline in every Bash block that needs it (each Bash call is a fresh shell); all copies must stay identical, which `scripts/review-pr-domain-map.test.sh` enforces.
|
|
1190
|
+
|
|
1175
1191
|
```bash
|
|
1176
1192
|
# Apply escalation triggers
|
|
1193
|
+
# BEGIN review-pr-agent-domain
|
|
1194
|
+
agent_domain() {
|
|
1195
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1196
|
+
security) echo security ;;
|
|
1197
|
+
auth) echo auth ;;
|
|
1198
|
+
billing) echo billing ;;
|
|
1199
|
+
concurrency) echo concurrency ;;
|
|
1200
|
+
database) echo database ;;
|
|
1201
|
+
infra|infrastructure) echo infra ;;
|
|
1202
|
+
scraper|scraping) echo scraper ;;
|
|
1203
|
+
frontend|web) echo frontend ;;
|
|
1204
|
+
api) echo api ;;
|
|
1205
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1206
|
+
esac
|
|
1207
|
+
}
|
|
1208
|
+
# END review-pr-agent-domain
|
|
1177
1209
|
add_agent() {
|
|
1178
|
-
local AGENT="
|
|
1179
|
-
|
|
1210
|
+
local AGENT CUR HAVE=""
|
|
1211
|
+
AGENT=$(agent_domain "${1}")
|
|
1212
|
+
for CUR in $SELECTED_AGENTS; do
|
|
1213
|
+
[ "$(agent_domain "$CUR")" = "$AGENT" ] && HAVE=1
|
|
1214
|
+
done
|
|
1215
|
+
[ -n "$HAVE" ] || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
|
|
1180
1216
|
}
|
|
1181
1217
|
|
|
1182
1218
|
[ "$SCORE_AUTH" -ge 3 ] && add_agent "Auth"
|
|
@@ -1412,6 +1448,34 @@ if [ "$THOROUGH" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then
|
|
|
1412
1448
|
fi
|
|
1413
1449
|
```
|
|
1414
1450
|
|
|
1451
|
+
**Roster canonicalization (final step of 3B, mandatory)**: rewrite `SELECTED_AGENTS` to canonical marker domains with one entry per persona, so `INFRA` and `Infrastructure` (or `SCRAPING` and `Scraping`) dispatch once and `SELECTED_AGENT_COUNT` counts each persona once. Pass the rewritten value to Phase 3C and Phase 4 as `{SELECTED_AGENTS}`. <!-- Added: forge#3485 -->
|
|
1452
|
+
|
|
1453
|
+
```bash
|
|
1454
|
+
# BEGIN review-pr-agent-domain
|
|
1455
|
+
agent_domain() {
|
|
1456
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1457
|
+
security) echo security ;;
|
|
1458
|
+
auth) echo auth ;;
|
|
1459
|
+
billing) echo billing ;;
|
|
1460
|
+
concurrency) echo concurrency ;;
|
|
1461
|
+
database) echo database ;;
|
|
1462
|
+
infra|infrastructure) echo infra ;;
|
|
1463
|
+
scraper|scraping) echo scraper ;;
|
|
1464
|
+
frontend|web) echo frontend ;;
|
|
1465
|
+
api) echo api ;;
|
|
1466
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1467
|
+
esac
|
|
1468
|
+
}
|
|
1469
|
+
# END review-pr-agent-domain
|
|
1470
|
+
CANON_ROSTER=""
|
|
1471
|
+
for AGENT in $SELECTED_AGENTS; do
|
|
1472
|
+
D=$(agent_domain "$AGENT")
|
|
1473
|
+
case " $CANON_ROSTER " in *" $D "*) ;; *) CANON_ROSTER="$CANON_ROSTER $D" ;; esac
|
|
1474
|
+
done
|
|
1475
|
+
SELECTED_AGENTS="${CANON_ROSTER# }"
|
|
1476
|
+
echo "=== CANONICAL ROSTER: $SELECTED_AGENTS ==="
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1415
1479
|
**Why cross-critical domain pairs always escalate**: A 2-file PR touching both `services/api/app/core/auth.py` and `services/api/app/routers/billing.py` creates interaction bugs that single-domain reviewers cannot catch. Never rely on a single agent for multi-domain risk.
|
|
1416
1480
|
|
|
1417
1481
|
**General Security agent ALWAYS runs.** If BILLING is selected, Concurrency is always added. If SHARED module is touched, add agents for all importing services.
|
|
@@ -1690,14 +1754,35 @@ if [ "$REVIEW_SHA_OK" != "true" ]; then
|
|
|
1690
1754
|
fi
|
|
1691
1755
|
WAIT_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/forge-review-wait-$(id -u)" # absolute and identical in every Bash call (cwd is not)
|
|
1692
1756
|
rm -f "${WAIT_DIR}/${PR_NUMBER}_${REVIEW_SHA}_review-wait-deadline" # a (re)dispatch always starts a fresh wait window
|
|
1757
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1693
1758
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1759
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1760
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1761
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
1762
|
+
done
|
|
1694
1763
|
TRUSTED_SCRIPT=""
|
|
1695
|
-
|
|
1764
|
+
while IFS= read -r _c; do
|
|
1696
1765
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1697
|
-
done
|
|
1766
|
+
done <<< "$_tc"
|
|
1698
1767
|
PENDING_AGENTS=""
|
|
1768
|
+
# BEGIN review-pr-agent-domain
|
|
1769
|
+
agent_domain() {
|
|
1770
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1771
|
+
security) echo security ;;
|
|
1772
|
+
auth) echo auth ;;
|
|
1773
|
+
billing) echo billing ;;
|
|
1774
|
+
concurrency) echo concurrency ;;
|
|
1775
|
+
database) echo database ;;
|
|
1776
|
+
infra|infrastructure) echo infra ;;
|
|
1777
|
+
scraper|scraping) echo scraper ;;
|
|
1778
|
+
frontend|web) echo frontend ;;
|
|
1779
|
+
api) echo api ;;
|
|
1780
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1781
|
+
esac
|
|
1782
|
+
}
|
|
1783
|
+
# END review-pr-agent-domain
|
|
1699
1784
|
for AGENT in $SELECTED_AGENTS; do
|
|
1700
|
-
AGENT_DOMAIN=$(
|
|
1785
|
+
AGENT_DOMAIN=$(agent_domain "$AGENT")
|
|
1701
1786
|
HAVE=""
|
|
1702
1787
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
1703
1788
|
HAVE=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
@@ -1709,7 +1794,7 @@ done
|
|
|
1709
1794
|
echo "PENDING_AGENTS:${PENDING_AGENTS:- (none)}"
|
|
1710
1795
|
```
|
|
1711
1796
|
|
|
1712
|
-
**CRITICAL**: Launch ALL agents in `PENDING_AGENTS` in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. If `PENDING_AGENTS` is empty, every selected reviewer already posted for this head: launch nothing and go straight to Phase 4. Each agent must persist its finalized body before posting it with `gh pr comment --body-file`, start the body with `<!-- FORGE:REVIEW-AGENT:{
|
|
1797
|
+
**CRITICAL**: Launch ALL agents in `PENDING_AGENTS` in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. If `PENDING_AGENTS` is empty, every selected reviewer already posted for this head: launch nothing and go straight to Phase 4. Each agent must persist its finalized body before posting it with `gh pr comment --body-file`, start the body with `<!-- FORGE:REVIEW-AGENT:{marker-domain} -->` (`{marker-domain}` is the canonical value from the roster-to-marker table, i.e. the `AGENT_DOMAIN` computed above for that agent; pass it to each persona explicitly) followed by a `Reviewed-SHA: ${REVIEW_SHA}` line (the full head SHA, outside the marker), and return its verdict and findings to the orchestrator independently of GitHub delivery.
|
|
1713
1798
|
|
|
1714
1799
|
**Dispatch failure and partial-panel guard (MANDATORY):** Count the selected roster before dispatch. If any launch fails, including from pool exhaustion, do not continue with the agents that did launch as a sufficient panel. Immediately run the **Panel hard-stop block** below with `ACTUAL_AGENT_COUNT=0` and exit without a verdict. After reviewers complete (joined `Task`/`task` return) or, under async `Agent`, after the Phase 4 bounded wait ends, independently compare their posted trusted current-SHA `FORGE:REVIEW-AGENT` markers with the selected count; a smaller count is the same hard stop. This catches a reviewer that accepted dispatch but failed before posting. Under async `Agent` the comparison runs only after the wait, so a reviewer that is merely late is not a false degraded stop.
|
|
1715
1800
|
|
|
@@ -1808,16 +1893,37 @@ if [ "$NOW_SHA" != "$REVIEW_SHA" ]; then
|
|
|
1808
1893
|
echo "PANEL_STATUS: STOP"; exit 0
|
|
1809
1894
|
fi
|
|
1810
1895
|
|
|
1896
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1811
1897
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1898
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1899
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1900
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
1901
|
+
done
|
|
1812
1902
|
TRUSTED_SCRIPT=""
|
|
1813
|
-
|
|
1903
|
+
while IFS= read -r _c; do
|
|
1814
1904
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1815
|
-
done
|
|
1905
|
+
done <<< "$_tc"
|
|
1816
1906
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1817
1907
|
MISSING_AGENT_COMMENTS=""
|
|
1818
1908
|
ACTUAL_AGENT_COUNT=0
|
|
1909
|
+
# BEGIN review-pr-agent-domain
|
|
1910
|
+
agent_domain() {
|
|
1911
|
+
case "$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')" in
|
|
1912
|
+
security) echo security ;;
|
|
1913
|
+
auth) echo auth ;;
|
|
1914
|
+
billing) echo billing ;;
|
|
1915
|
+
concurrency) echo concurrency ;;
|
|
1916
|
+
database) echo database ;;
|
|
1917
|
+
infra|infrastructure) echo infra ;;
|
|
1918
|
+
scraper|scraping) echo scraper ;;
|
|
1919
|
+
frontend|web) echo frontend ;;
|
|
1920
|
+
api) echo api ;;
|
|
1921
|
+
*) printf '%s' "${1}" | tr '[:upper:]' '[:lower:]' ;;
|
|
1922
|
+
esac
|
|
1923
|
+
}
|
|
1924
|
+
# END review-pr-agent-domain
|
|
1819
1925
|
for AGENT in $SELECTED_AGENTS; do
|
|
1820
|
-
AGENT_DOMAIN=$(
|
|
1926
|
+
AGENT_DOMAIN=$(agent_domain "$AGENT")
|
|
1821
1927
|
N=""
|
|
1822
1928
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
1823
1929
|
N=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)" 2>/dev/null || echo "")
|
|
@@ -1860,11 +1966,16 @@ case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA
|
|
|
1860
1966
|
# CURRENT head. The head SHA is public, so the SHA line alone proves nothing: a bare contains() selection would let any
|
|
1861
1967
|
# commenter forge FINDING markers that get filed as issues or inflate the agent count. Earlier-head comments are excluded
|
|
1862
1968
|
# so a re-entry or head move does not re-file already-fixed findings. Uses jq scan() (POSIX-portable, no PCRE grep).
|
|
1969
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1863
1970
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
1971
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
1972
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
1973
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
1974
|
+
done
|
|
1864
1975
|
TRUSTED_SCRIPT=""
|
|
1865
|
-
|
|
1976
|
+
while IFS= read -r _c; do
|
|
1866
1977
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1867
|
-
done
|
|
1978
|
+
done <<< "$_tc"
|
|
1868
1979
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1869
1980
|
AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)"
|
|
1870
1981
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
@@ -1904,11 +2015,16 @@ case "$REVIEW_SHA" in *[!0-9a-f]*|"") REVIEW_SHA_OK=false ;; *) [ "${#REVIEW_SHA
|
|
|
1904
2015
|
# Only TRUSTED bodies for the CURRENT head count (scripts/trusted-comments.sh + anchored marker + Reviewed-SHA line):
|
|
1905
2016
|
# findings from earlier heads are already fixed or re-reviewed, and an untrusted commenter must never be able to inject
|
|
1906
2017
|
# findings or post a forged synthesis block that makes the review read clean (the head SHA is public).
|
|
2018
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1907
2019
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2020
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2021
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2022
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
2023
|
+
done
|
|
1908
2024
|
TRUSTED_SCRIPT=""
|
|
1909
|
-
|
|
2025
|
+
while IFS= read -r _c; do
|
|
1910
2026
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1911
|
-
done
|
|
2027
|
+
done <<< "$_tc"
|
|
1912
2028
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
1913
2029
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$COMMENTS_JSON" ]; then
|
|
1914
2030
|
echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read clean (fail closed)"; exit 0
|
|
@@ -1978,11 +2094,16 @@ fi
|
|
|
1978
2094
|
# fix round on this PR. Any trusted FORGE:INPR_FIX* marker already on the PR means the round is used.
|
|
1979
2095
|
INPR_DIFF_FILE=""
|
|
1980
2096
|
if [ "${AUTO_MERGE:-false}" = "true" ] && [ -n "${MERGE_ISSUE:-}" ]; then
|
|
2097
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
1981
2098
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2099
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2100
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2101
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
2102
|
+
done
|
|
1982
2103
|
TRUSTED_SCRIPT=""
|
|
1983
|
-
|
|
2104
|
+
while IFS= read -r _c; do
|
|
1984
2105
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
1985
|
-
done
|
|
2106
|
+
done <<< "$_tc"
|
|
1986
2107
|
if [ -n "$TRUSTED_SCRIPT" ]; then INPR_MARKERS=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null \
|
|
1987
2108
|
| bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:INPR_FIX' 2>/dev/null || echo ""); else INPR_MARKERS=""; fi
|
|
1988
2109
|
if [ "$INPR_MARKERS" = "0" ]; then
|
|
@@ -2637,11 +2758,16 @@ fi
|
|
|
2637
2758
|
FINDING_COUNT=$(printf '%s' "$DISPO_JSON" | jq -s '[.[][] | select(.body | test("<!-- FINDING:"))] | length')
|
|
2638
2759
|
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh, same as verify-phase-trail.sh): trusted association, Bot account, or
|
|
2639
2760
|
# FORGE_TRAIL_TRUSTED_LOGINS. A GitHub App bot always has author_association NONE, so an association-only filter drops the pipeline's own markers.
|
|
2761
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
2640
2762
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2763
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2764
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2765
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
2766
|
+
done
|
|
2641
2767
|
TRUSTED_SCRIPT=""
|
|
2642
|
-
|
|
2768
|
+
while IFS= read -r _c; do
|
|
2643
2769
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
2644
|
-
done
|
|
2770
|
+
done <<< "$_tc"
|
|
2645
2771
|
if [ -z "$TRUSTED_SCRIPT" ] || [ -z "$FINDING_COUNT" ]; then
|
|
2646
2772
|
echo "NOTE DISPOSITION: scripts/trusted-comments.sh unresolvable or findings unreadable — refusing to merge (fail closed)" >&2
|
|
2647
2773
|
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "note disposition unreadable".
|
|
@@ -2794,8 +2920,8 @@ if [ "${BASE_CONFLICT:-false}" = "true" ] && [ "${OTHER_BLOCKING:-false}" != "tr
|
|
|
2794
2920
|
fi
|
|
2795
2921
|
if [ "${ONLY_BASE_CONFLICT:-false}" = "true" ] && [ "$VERDICT" = "CHANGES REQUESTED" ]; then
|
|
2796
2922
|
# Pure base conflict (set in Phase 7B: the ONLY blocker): route to the base-sync path, NOT needs-human. <!-- Added: forge#3496 -->
|
|
2797
|
-
# work-on/review.md R4
|
|
2798
|
-
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict with \`{MERGE_BASE}\` (the only blocking reason).
|
|
2923
|
+
# work-on/review.md R4 sets workflow:remediating (not needs-human) when it hands off to remediate; a standalone /review-pr caller just sees the blocker.
|
|
2924
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict with \`{MERGE_BASE}\` (the only blocking reason). No operator action is needed: the pipeline is performing the base sync automatically (merging \`origin/{MERGE_BASE}\` into the PR branch, merge only, no rebase, no force-push), re-running the quality gate, then re-reviewing the new head. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
2799
2925
|
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "base-conflict". Do NOT add needs-human and do NOT attempt gh pr merge.
|
|
2800
2926
|
elif [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "${TRUST_NEEDS_HUMAN:-false}" = "true" ]; then
|
|
2801
2927
|
BLOCK_REASON=""
|
|
@@ -2825,7 +2951,7 @@ fi
|
|
|
2825
2951
|
|
|
2826
2952
|
if [ "$PRE_MERGE_BASE_CONFLICT" = "true" ]; then
|
|
2827
2953
|
# Base moved during review: route to the base-sync path, NOT needs-human. <!-- Added: forge#3496 -->
|
|
2828
|
-
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`).
|
|
2954
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`). No operator action is needed: the pipeline is performing the base sync automatically (merging \`origin/{MERGE_BASE}\` into the PR branch, merge only, no rebase, no force-push) and then re-reviewing. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
2829
2955
|
# STOP — return REVIEW_RESULT: status: BLOCKED, blocker: "base-conflict". Do NOT add needs-human and do NOT attempt gh pr merge.
|
|
2830
2956
|
elif [ "$PRE_MERGE_HEALTH_STATE" = "BLOCKED" ]; then
|
|
2831
2957
|
# Branch protection not satisfied: syncing the base cannot clear it, so this is NOT a base-conflict blocker.
|
|
@@ -2852,7 +2978,7 @@ else
|
|
|
2852
2978
|
# bot-only re-review (authorAssociation=NONE) could never satisfy the strict ≥2 verified-
|
|
2853
2979
|
# human bar and so stranded staging PRs the fast lane would auto-merge.
|
|
2854
2980
|
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels,comments \
|
|
2855
|
-
--jq '([.labels[].name | . == "needs-human"] + [.comments[].body | contains("FORGE:REMEDIATION")]) | any' 2>/dev/null || echo "false")
|
|
2981
|
+
--jq '([.labels[].name | . == "needs-human" or . == "workflow:remediating"] + [.comments[].body | contains("FORGE:REMEDIATION")]) | any' 2>/dev/null || echo "false")
|
|
2856
2982
|
GUARD_BASE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2857
2983
|
|
|
2858
2984
|
if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
@@ -2876,7 +3002,7 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
2876
3002
|
adaptive|universal) bash "$SCRIPT_PATH" {MERGE_ISSUE} {MERGE_GH_FLAG} awaiting-merge ;;
|
|
2877
3003
|
prose)
|
|
2878
3004
|
gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "workflow:awaiting-merge" \
|
|
2879
|
-
--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
|
|
3005
|
+
--remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
2880
3006
|
;;
|
|
2881
3007
|
esac
|
|
2882
3008
|
# STOP — do not attempt gh pr merge here. The merge decision belongs to a human for a
|
|
@@ -3112,11 +3238,16 @@ A degraded run that skipped Task-based agent dispatch must be visible from this
|
|
|
3112
3238
|
|
|
3113
3239
|
```bash
|
|
3114
3240
|
# Same trust rule as Phase 3C/4: only trusted comments whose body starts with the marker count.
|
|
3241
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
3115
3242
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
3243
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
3244
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
3245
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
3246
|
+
done
|
|
3116
3247
|
TRUSTED_SCRIPT=""
|
|
3117
|
-
|
|
3248
|
+
while IFS= read -r _c; do
|
|
3118
3249
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
3119
|
-
done
|
|
3250
|
+
done <<< "$_tc"
|
|
3120
3251
|
ACTUAL_AGENT_DOMAINS=""
|
|
3121
3252
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
3122
3253
|
ACTUAL_AGENT_DOMAINS=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
@@ -42,7 +42,7 @@ Parse from $ARGUMENTS:
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason.
|
|
45
|
+
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
|
|
46
46
|
|
|
47
47
|
## Phase I1: Load Context from GitHub
|
|
48
48
|
|
|
@@ -336,7 +336,7 @@ case "$TIER" in
|
|
|
336
336
|
prose)
|
|
337
337
|
[ -n "${DRY_RUN:-}" ] && echo "DRY_RUN: set workflow:building" || \
|
|
338
338
|
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
|
|
339
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
339
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
340
340
|
;;
|
|
341
341
|
esac
|
|
342
342
|
```
|
|
@@ -529,7 +529,12 @@ Review findings per PR rise steeply with diff size, so an oversized diff is spli
|
|
|
529
529
|
|
|
530
530
|
```bash
|
|
531
531
|
# <Script resolution block from above goes here, verbatim>
|
|
532
|
-
|
|
532
|
+
# Fresh origin/base for the merge-base measurement: retry with 10s/30s/60s backoff, record whether it ever succeeded
|
|
533
|
+
FETCH_OK=0
|
|
534
|
+
for _delay in 0 10 30 60; do
|
|
535
|
+
[ "$_delay" -gt 0 ] && sleep "$_delay"
|
|
536
|
+
if git -C "{WORKTREE_PATH}" fetch origin "{PR_BASE}" 2>/dev/null; then FETCH_OK=1; break; fi
|
|
537
|
+
done
|
|
533
538
|
THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)
|
|
534
539
|
EXCLUDES=()
|
|
535
540
|
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
@@ -539,12 +544,13 @@ case "$SCRIPT_REF" in
|
|
|
539
544
|
prose:*) echo "SIZE_GATE: scripts/diff-size.sh unresolved (prose tier) — gate skipped with a note in the FORGE:BUILDER comment" ;;
|
|
540
545
|
*)
|
|
541
546
|
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --threshold "$THRESHOLD" ${EXCLUDES[@]+"${EXCLUDES[@]}"}) && SIZE_RC=0 || SIZE_RC=$?
|
|
542
|
-
echo "SIZE_RC=${SIZE_RC}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
547
|
+
echo "SIZE_RC=${SIZE_RC}"; echo "FETCH_OK=${FETCH_OK}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
543
548
|
esac
|
|
544
549
|
```
|
|
545
550
|
|
|
546
551
|
- Prose tier (script unresolvable) → skip the gate, continue to B6 (the script is part of the install; a missing install must not strand every build).
|
|
547
|
-
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over
|
|
552
|
+
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over=`, AND `FETCH_OK=0` (the base fetch failed after the 10s/30s/60s retries, worst case about 100s) → Blocked exit with blocker `github-unavailable: could not fetch origin/{PR_BASE} for diff-size measurement` (transient; the router retry rule applies).
|
|
553
|
+
- The same unmeasurable result with `FETCH_OK=1` (the fetch succeeded, so the script itself failed) → Blocked exit with blocker `size-gate-unavailable: diff-size.sh failed (rc=<N>)`. Either way, do NOT treat an unmeasurable diff as under threshold.
|
|
548
554
|
- Otherwise parse `diff_lines`, `excluded_lines`, `threshold`, `over` and the `top=` lines.
|
|
549
555
|
|
|
550
556
|
**Step 2 — look up an override** (only when `over=true`). Both lookups are anchored, trust-filtered through `scripts/trusted-comments.sh` (trusted association, Bot, or `FORGE_TRAIL_TRUSTED_LOGINS`), and fail closed:
|
|
@@ -782,6 +788,7 @@ fi
|
|
|
782
788
|
If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
|
|
783
789
|
|
|
784
790
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
791
|
+
1.5. **Decomposition guard** <!-- Added: forge#3543 -->: read the `### Decomposition Assessment` of the latest FORGE:INVESTIGATOR comment. If it is `**YES**`, a failing check that belongs to a sub-item the assessment assigns to a separate issue (named in the check's `description`/`# item-N` tag, or matching a listed sub-issue title) is NOT repaired: exclude it from the `--fix-acceptance` list. When ambiguous, treat the check as decomposed-out (fail safe). If every remaining failure is decomposed-out, do not invoke implement: leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "acceptance check {ID} belongs to decomposed item {N} — repair refuses to implement work the investigator assigned to a separate issue".
|
|
785
792
|
2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
|
|
786
793
|
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
|
|
787
794
|
then re-derive `{CHANGED_FILES}` from the repaired worktree (never reuse the pre-repair value, and never omit `--files`; an empty list falls back to this diff):
|
|
@@ -20,7 +20,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
|
20
20
|
**Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
|
|
21
21
|
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`. STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
|
|
22
22
|
|
|
23
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are 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
|
|
23
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are 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 -->` (or a `**YES**` verdict under `### Decomposition Assessment`) in the newest trusted FORGE:INVESTIGATOR report, instead of terminating in place, so a headless `runIssue()` walk dispatches this subcommand rather than stopping short.
|
|
24
24
|
|
|
25
25
|
**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. <!-- Added: forge#1827 -->
|
|
26
26
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -513,7 +513,7 @@ case "$TIER" in
|
|
|
513
513
|
adaptive|universal) run bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
|
|
514
514
|
prose)
|
|
515
515
|
run gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
|
|
516
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
516
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
517
517
|
;;
|
|
518
518
|
esac
|
|
519
519
|
```
|
|
@@ -416,7 +416,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
416
416
|
case "$TIER" in
|
|
417
417
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
418
418
|
prose)
|
|
419
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
419
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
420
420
|
;;
|
|
421
421
|
esac
|
|
422
422
|
```
|
|
@@ -609,7 +609,7 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
609
609
|
|
|
610
610
|
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 -->`:**
|
|
611
611
|
|
|
612
|
-
- **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
|
|
612
|
+
- **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 reads it as the closing line of the newest trusted `<!-- FORGE:INVESTIGATOR -->` comment (never as a substring of prose or from an untrusted author) 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).
|
|
613
613
|
- **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).
|
|
614
614
|
|
|
615
615
|
Compute the sentinel once, before building the comment body:
|
|
@@ -635,6 +635,9 @@ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
|
635
635
|
--field "Task Type={TASK_TYPE}" \
|
|
636
636
|
--field "Decomposition Assessment={YES|NO} — {reason}")
|
|
637
637
|
# ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
|
|
638
|
+
# After appending the body sections, the Decomposition Assessment MUST be followed by exactly one
|
|
639
|
+
# machine-readable marker line (`<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->`) — see the
|
|
640
|
+
# "Decomposition marker" rule below the template. The codec does not add it for you.
|
|
638
641
|
# NOTE: the codec's sentinel is fixed — do not use this path when Verdict=INVALID (see caveat above).
|
|
639
642
|
# Append the Markdown body sections to it before posting.
|
|
640
643
|
```
|
|
@@ -700,6 +703,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
700
703
|
### Decomposition Assessment
|
|
701
704
|
**{YES|NO}** — {reason}
|
|
702
705
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
706
|
+
<!-- DECOMPOSE:{YES|NO} -->
|
|
703
707
|
|
|
704
708
|
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
705
709
|
{For each item in the issue's ## Acceptance Criteria section, emit one machine-checkable check line using the format below. If the issue has no Acceptance Criteria section, derive checks from the Recommendation above. Each check MUST be specific, observable, and testable — not vague prose. Checks are consumed by build/validate Phase B6.5 as the merge gate.}
|
|
@@ -731,6 +735,12 @@ ${ANNOTATION_LINK_FOOTER}
|
|
|
731
735
|
${INVESTIGATION_SENTINEL}"
|
|
732
736
|
```
|
|
733
737
|
|
|
738
|
+
**Rules for the template above (instructions, not template text; never copy them into the posted comment):**
|
|
739
|
+
|
|
740
|
+
**Decomposition marker (MANDATORY, both output paths — template and codec)**: emit exactly one `<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->` line per investigator comment, matching the `**YES**`/`**NO**` verdict, on its own line (nothing else on that line) after the verdict line and any sub-issue list (the `**YES**`/`**NO**` line must stay directly under the heading — the resume parser reads it). The headless engine routes to `work-on/decompose` on this marker (or on the `**YES**` heading in older comments). Never quote or mention the opposite marker anywhere in the comment. Never emit it when Verdict=INVALID. <!-- Added: forge#3543 -->
|
|
741
|
+
|
|
742
|
+
**Decomposition scopes the Acceptance Spec**: when the assessment is YES, the Acceptance Spec MUST cover only the first, in-scope item — or tag each check with the sub-item it belongs to (append `# item-N` to the description). Never emit checks for work the assessment assigns to a separate sub-issue: build's acceptance gate would otherwise fail on them and its repair loop would pull that work into this branch. <!-- Added: forge#3543 -->
|
|
743
|
+
|
|
734
744
|
**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.
|
|
735
745
|
|
|
736
746
|
---
|
|
@@ -1183,7 +1193,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1183
1193
|
case "$TIER" in
|
|
1184
1194
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
1185
1195
|
prose)
|
|
1186
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1196
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1187
1197
|
;;
|
|
1188
1198
|
esac
|
|
1189
1199
|
```
|
|
@@ -1203,7 +1213,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1203
1213
|
case "$TIER" in
|
|
1204
1214
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
1205
1215
|
prose)
|
|
1206
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1216
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1207
1217
|
;;
|
|
1208
1218
|
esac
|
|
1209
1219
|
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}" # allowlist:check-command-side-effects
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
user-invocable: false
|
|
3
|
-
description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
3
|
+
description: Remediate subcommand — checkout a needs-human or workflow:remediating PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
4
4
|
argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
|
|
5
5
|
context: fork
|
|
6
6
|
background: false
|
|
@@ -19,7 +19,7 @@ background: false
|
|
|
19
19
|
|
|
20
20
|
**Invoked by**:
|
|
21
21
|
- `work-on.md` Phase 0A.1 (router), standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
22
|
-
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
|
|
22
|
+
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated or `workflow:remediating` predecessor's own open PR.
|
|
23
23
|
- `work-on.md` Phase 4R (router), when the review phase hands off a red CI gate or an in-PR fix request (`REVIEW_RESULT: status: NEXT`).
|
|
24
24
|
|
|
25
25
|
**Only the router invokes this skill** (forge#3398, `work-on.md` Hard Rule 1a). It re-reviews through `/review-pr`, which spawns domain reviewers, so it must run one level below the router. No phase may invoke it from inside its own fork.
|
|
@@ -29,7 +29,7 @@ background: false
|
|
|
29
29
|
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
|
|
30
30
|
**NEVER use plan mode (EnterPlanMode).**
|
|
31
31
|
|
|
32
|
-
**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.
|
|
32
|
+
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` or `workflow:remediating` PR's own remediation (the latter is the non-human "autonomous remediation pending" state review sets for ci-gate, in-pr-fix and base-sync handoffs, forge#3541). 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.
|
|
33
33
|
|
|
34
34
|
**Engine coverage** (forge#2379, #2889): 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 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. A blocked review is committed with `terminalReason: "needs-human"`, then the engine continues directly into remediation; the divergence guard permits this specific handoff while keeping all other `needs-human` states paused.
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ background: false
|
|
|
38
38
|
## Inputs
|
|
39
39
|
|
|
40
40
|
Parse from $ARGUMENTS:
|
|
41
|
-
- `{PR_NUMBER}` — PR number to remediate (required, first positional arg). This is the `needs-human`-gated PR itself, NOT the linked issue number.
|
|
41
|
+
- `{PR_NUMBER}` — PR number to remediate (required, first positional arg). This is the `needs-human`-gated or `workflow:remediating` PR itself, NOT the linked issue number.
|
|
42
42
|
- `--issue {ISSUE_NUMBER}` — linked issue number (optional). If absent, resolved in Phase M0 from the PR body's `Closes #N` reference.
|
|
43
43
|
- `--repo {GH_REPO}` — GitHub repo (resolved from `forge.yaml → project` if omitted)
|
|
44
44
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag
|
|
@@ -79,7 +79,12 @@ ISSUE_STATE=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels,state,body,mi
|
|
|
79
79
|
ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
- If `needs-human` is
|
|
82
|
+
- If neither `needs-human` nor `workflow:remediating` is among `ISSUE_LABELS` → EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "issue #{ISSUE_NUMBER} is neither `needs-human` nor `workflow:remediating` — remediation mode only targets PRs in one of those two states; use the normal `/work-on {ISSUE_NUMBER}` resume path instead." This keeps blast radius scoped to exactly the gap this mode fills — it is not a general-purpose re-review trigger. `workflow:remediating` is the non-human state review sets when it hands an autonomous ci-gate / in-pr-fix / base-sync fix to this phase (forge#3541); a bare `needs-human` (older runs, genuine escalations being re-driven) is still accepted. <!-- Added: forge#3541 -->
|
|
83
|
+
- **Stranded-state rule**: `workflow:remediating` must never outlive this run. Every exit that returns `BLOCKED` or `UNFIXABLE` (including exits before Phase M1) removes `workflow:remediating` and adds `needs-human`, because at that point a human really is needed:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
87
|
+
```
|
|
83
88
|
|
|
84
89
|
**Idempotency / resume check** — the paper trail lives on **both** the PR (primary — checked by the orchestrator's item 6.4 dispatch guard) and the linked issue (mirror — keeps `/work-on`'s standard FORGE-annotation trajectory and resume logic consistent with every other phase):
|
|
85
90
|
|
|
@@ -87,11 +92,16 @@ ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
|
87
92
|
# Trust predicate: ONE shared copy (scripts/trusted-comments.sh). Markers are matched as the comment's LEADING
|
|
88
93
|
# text (^ anchors at the start of the body, not of a line), never with a bare contains(): a review comment or
|
|
89
94
|
# an INPR_FIX work order that merely QUOTES FORGE:REMEDIATION:COMPLETE must not read as a remediation trail (forge#3412).
|
|
95
|
+
# TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
90
96
|
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
97
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
98
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
99
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(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)"
|
|
100
|
+
done
|
|
91
101
|
TRUSTED_SCRIPT=""
|
|
92
|
-
|
|
102
|
+
while IFS= read -r _c; do
|
|
93
103
|
case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
|
|
94
|
-
done
|
|
104
|
+
done <<< "$_tc"
|
|
95
105
|
PR_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{PR_NUMBER}/comments 2>/dev/null) || PR_COMMENTS=""
|
|
96
106
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ]; then
|
|
97
107
|
# Real trail comments (M5 interim, M8 final) START with <!-- FORGE:REMEDIATION -->; M8 also carries <!-- FORGE:REMEDIATION:COMPLETE -->.
|
|
@@ -152,17 +162,17 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
152
162
|
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
153
163
|
- **UNFIXABLE (policy escalation)** — `HAS_PURPOSE_REGRESSION=true` (the PR's behavior diverges from the issue's intent — a judgment call, not a code defect), `CALIBRATION_NEEDS_HUMAN=true` (statistical trust threshold), or `TRUST_NEEDS_HUMAN=true` (provenance `NOVEL_NEEDS_HUMAN` tier, insufficient prior data — a policy gate, not a bug). None of these are mechanically "fixable" by re-editing code.
|
|
154
164
|
|
|
155
|
-
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (it
|
|
165
|
+
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (add it and remove `workflow:remediating` if the issue entered as `workflow:remediating`), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
156
166
|
|
|
157
|
-
**If at least one FIXABLE item exists**: transition the issue out of its terminal gate before proceeding to Phase M2. `needs-human` represents the prior review result, not an active automated remediation run; retaining it would make the dispatcher and recovery paths stop while remediation is in progress. Keep exactly one active workflow state:
|
|
167
|
+
**If at least one FIXABLE item exists**: transition the issue out of its terminal gate before proceeding to Phase M2. `needs-human` (or the `workflow:remediating` handoff state) represents the prior review result, not an active automated remediation run; retaining it would make the dispatcher and recovery paths stop while remediation is in progress. Keep exactly one active workflow state:
|
|
158
168
|
|
|
159
169
|
```bash
|
|
160
170
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
161
|
-
echo "DRY_RUN: would replace needs-human with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
171
|
+
echo "DRY_RUN: would replace needs-human / workflow:remediating with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
162
172
|
else
|
|
163
173
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} \
|
|
164
174
|
--add-label "workflow:in-review" \
|
|
165
|
-
--remove-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
175
|
+
--remove-label "needs-human,workflow:remediating" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
166
176
|
fi
|
|
167
177
|
```
|
|
168
178
|
|
|
@@ -491,7 +501,7 @@ if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
|
491
501
|
adaptive|universal) bash "$SCRIPT_PATH" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
492
502
|
prose)
|
|
493
503
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" \
|
|
494
|
-
--remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
504
|
+
--remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
495
505
|
;;
|
|
496
506
|
esac
|
|
497
507
|
RE_GATE_OUTCOME="AUTO-LANDED"
|