forgedock 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/phases-args.test.mjs +11 -0
- package/bin/engine/phases.mjs +115 -14
- package/bin/engine/phases.test.mjs +24 -0
- package/bin/engine.mjs +41 -7
- package/bin/forgedock.mjs +1 -1
- package/commands/issue.md +17 -2
- package/commands/orchestrate/phase-4-execution.md +10 -3
- package/commands/review-pr-staging.md +278 -28
- package/commands/review-pr.md +71 -22
- package/commands/work-on/build/implement.md +15 -1
- package/commands/work-on/build.md +7 -4
- package/commands/work-on/investigate.md +5 -3
- package/commands/work-on/remediate.md +215 -35
- package/docs/CONFIG.md +2 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +14 -7
- package/scripts/classify-finding.test.sh +9 -0
- package/scripts/forge-root.test.sh +41 -4
- package/scripts/remediate-approved-head.test.sh +43 -0
- package/scripts/remediation-marker-match.test.sh +6 -0
- package/scripts/resolve-deploy-gate.sh +51 -0
- package/scripts/resolve-deploy-gate.test.sh +59 -0
- package/scripts/review-pr-delta-wiring.test.sh +8 -0
- package/scripts/review-pr-domain-map.test.sh +10 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
package/commands/review-pr.md
CHANGED
|
@@ -1520,7 +1520,7 @@ Each agent receives only its domain-relevant diff slice rather than the full cha
|
|
|
1520
1520
|
# Domain file patterns — used in Phase 3C to scope each agent's input
|
|
1521
1521
|
DOMAIN_FILES_AUTH=$(echo "$FILES" | grep -iE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" || echo "")
|
|
1522
1522
|
DOMAIN_FILES_BILLING=$(echo "$FILES" | grep -iE "billing|credit|payment|charge|refund|subscription|pricing|tier" || echo "")
|
|
1523
|
-
DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis" || echo "")
|
|
1523
|
+
DOMAIN_FILES_CONCURRENCY=$(echo "$FILES" | grep -iE "lock|queue|atomic|transaction|worker|job|task|celery|redis|claim|lease|engine-state" || echo "")
|
|
1524
1524
|
DOMAIN_FILES_DATABASE=$(echo "$FILES" | grep -iE "migration|\.sql$|model|schema|db|database|alembic" || echo "")
|
|
1525
1525
|
DOMAIN_FILES_INFRA=$(echo "$FILES" | grep -iE "docker|nginx|traefik|\.github|Makefile|deploy|infra" || echo "")
|
|
1526
1526
|
DOMAIN_FILES_SCRAPING=$(echo "$FILES" | grep -iE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" || echo "")
|
|
@@ -1528,10 +1528,15 @@ DOMAIN_FILES_FRONTEND=$(echo "$FILES" | grep -iE "^web/|\.tsx?$|\.jsx?$|componen
|
|
|
1528
1528
|
DOMAIN_FILES_API=$(echo "$FILES" | grep -iE "router|route|endpoint|openapi|sdk|api" || echo "")
|
|
1529
1529
|
|
|
1530
1530
|
# Fallback: if a domain has no specific files matched, give it the full file list
|
|
1531
|
-
# (
|
|
1531
|
+
# (a domain can be on the roster without a pattern hit, e.g. via delta_roster or Security's full-diff scope)
|
|
1532
1532
|
[ -z "$DOMAIN_FILES_AUTH" ] && DOMAIN_FILES_AUTH="$FILES"
|
|
1533
1533
|
[ -z "$DOMAIN_FILES_BILLING" ] && DOMAIN_FILES_BILLING="$FILES"
|
|
1534
|
+
[ -z "$DOMAIN_FILES_CONCURRENCY" ] && DOMAIN_FILES_CONCURRENCY="$FILES"
|
|
1534
1535
|
[ -z "$DOMAIN_FILES_DATABASE" ] && DOMAIN_FILES_DATABASE="$FILES"
|
|
1536
|
+
[ -z "$DOMAIN_FILES_INFRA" ] && DOMAIN_FILES_INFRA="$FILES"
|
|
1537
|
+
[ -z "$DOMAIN_FILES_SCRAPING" ] && DOMAIN_FILES_SCRAPING="$FILES"
|
|
1538
|
+
[ -z "$DOMAIN_FILES_FRONTEND" ] && DOMAIN_FILES_FRONTEND="$FILES"
|
|
1539
|
+
[ -z "$DOMAIN_FILES_API" ] && DOMAIN_FILES_API="$FILES"
|
|
1535
1540
|
```
|
|
1536
1541
|
|
|
1537
1542
|
### 3C: Load Agent Templates & Launch
|
|
@@ -1612,9 +1617,11 @@ carry_verified_from() {
|
|
|
1612
1617
|
# BEGIN review-pr-delta-roster
|
|
1613
1618
|
# Effective roster for a DELTA head. While fewer than two full panels have run (FULL_ROUNDS < 2) the 3B roster stands;
|
|
1614
1619
|
# from the third review on it narrows to Security plus the domains whose trigger paths the delta touches.
|
|
1620
|
+
# spec-cli is internal-only (not shipped to consumers): it is added only when the persona file exists in the repo under review.
|
|
1615
1621
|
# Args: FULL_ROUNDS, the canonical 3B roster (space separated), the delta file list (one path per line).
|
|
1616
1622
|
delta_roster() {
|
|
1617
|
-
local rounds="${1}" full="${2}" files="${3}" out="security"
|
|
1623
|
+
local rounds="${1}" full="${2}" files="${3}" out="security" root
|
|
1624
|
+
root="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
1618
1625
|
case "$rounds" in ''|*[!0-9]*) printf '%s' "$full"; return 0 ;; esac
|
|
1619
1626
|
if [ "$rounds" -lt 2 ]; then printf '%s' "$full"; return 0; fi
|
|
1620
1627
|
printf '%s\n' "$files" | grep -qiE "auth|session|login|logout|jwt|oauth|token|user|permission|middleware" && out="$out auth"
|
|
@@ -1622,10 +1629,10 @@ delta_roster() {
|
|
|
1622
1629
|
printf '%s\n' "$files" | grep -qiE "lock|queue|atomic|transaction|worker|job|task|celery|redis|claim|lease|engine-state" && out="$out concurrency"
|
|
1623
1630
|
printf '%s\n' "$files" | grep -qiE "migration|\.sql$|model|schema|db|database|alembic" && out="$out database"
|
|
1624
1631
|
printf '%s\n' "$files" | grep -qiE "docker|nginx|traefik|\.github|Makefile|deploy|infra" && out="$out infra"
|
|
1625
|
-
printf '%s\n' "$files" | grep -qiE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|captcha" && out="$out scraper"
|
|
1632
|
+
printf '%s\n' "$files" | grep -qiE "scrape|crawler|stealth|browser.*pool|headless|anti.bot|detection.*keyword|captcha" && out="$out scraper"
|
|
1626
1633
|
printf '%s\n' "$files" | grep -qiE "^web/|\.tsx?$|\.jsx?$|component|page|layout|style|css" && out="$out frontend"
|
|
1627
1634
|
printf '%s\n' "$files" | grep -qiE "router|route|endpoint|openapi|sdk|api" && out="$out api"
|
|
1628
|
-
printf '%s\n' "$files" | grep -qE "^(commands|bin)/" && out="$out spec-cli"
|
|
1635
|
+
printf '%s\n' "$files" | grep -qE "^(commands|bin)/" && [ -f "$root/commands/review-pr-agents/spec-cli.md" ] && out="$out spec-cli"
|
|
1629
1636
|
printf '%s' "$out"
|
|
1630
1637
|
}
|
|
1631
1638
|
# END review-pr-delta-roster
|
|
@@ -1800,7 +1807,7 @@ Read: $MONOLITHIC_CATALOG
|
|
|
1800
1807
|
```
|
|
1801
1808
|
Then extract the shared protocols section and each selected persona's section from within that single file — same content, different packaging.
|
|
1802
1809
|
|
|
1803
|
-
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`, and `spec-cli.md` (marker domain `spec-cli`, only ever on a `DELTA` roster whose delta touches `commands/**` or `bin/**`).
|
|
1810
|
+
Persona file names: `security.md` (always), `auth.md`, `billing.md`, `concurrency.md`, `scraper.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`, and `spec-cli.md` (marker domain `spec-cli`, only ever on a `DELTA` roster whose delta touches `commands/**` or `bin/**` AND whose repo contains `commands/review-pr-agents/spec-cli.md`, i.e. ForgeDock itself; consumer repos never get it).
|
|
1804
1811
|
See `review-pr-agents.md` for the full routing table mapping domains → persona files.
|
|
1805
1812
|
|
|
1806
1813
|
(`$FORGE_HOME` defaults to `~/.claude`)
|
|
@@ -2553,7 +2560,7 @@ Filing a standalone `review-finding` issue for every LOW/POSSIBLE reviewer note
|
|
|
2553
2560
|
- **Safety exemption is content-based** (it bypasses only this severity damper, never the §6B.4 provenance gate: a pre-existing auth gap is flagged through the pre-existing route, not attributed to this PR): a LOW or POSSIBLE finding is still filed when its file path / title / body matches the security/billing keyword set — a LOW finding additionally needs `**Confidence**: CONFIRMED`, so LOW LIKELY/POSSIBLE with a keyword is a NOTE (`security|auth|authz|authn|billing|payment|stripe|charge|invoice|injection|xss|csrf|ssrf|idor|secret(s)|credential(s)|permission(s)|sql|token|password|redact`, whole words, `_`/`-`/`/` separate words — so `auth_service` matches but `author`/`tokenizer` do not), or when it came from a dedicated, signal-selected domain agent (Auth, Billing, Concurrency, Database) AND is MEDIUM — a domain agent rescues MEDIUM only, and a LOW finding stays a NOTE whichever reviewer raised it, CONFIRMED or not. **Origin from the always-on General Security & Quality agent alone does NOT exempt a finding**: that agent runs on every PR, so origin-based exemption filed nearly every LOW note it raised (the 2026-10-08 cascade audit: 50 of 60 would-be notes in one batch were filed only for that reason).
|
|
2554
2561
|
- **Review-finding lineage** (the PR's linked issue `MERGE_ISSUE` carries the `review-finding` label, any priority — this PR is itself a fix for a finding): only MEDIUM findings that are CONFIRMED, or LIKELY and safety-exempt, become issues; LOW and POSSIBLE are always NOTEs. A fix for a finding must not mint a new generation of findings.
|
|
2555
2562
|
- **Otherwise**: `**Severity**: LOW`, or `**Confidence**: POSSIBLE` below HIGH, is a NOTE unless safety-exempt (LOW: keyword AND CONFIRMED; MEDIUM: keyword at any confidence, or a domain agent); everything else (MEDIUM CONFIRMED/LIKELY) is an ISSUE.
|
|
2556
|
-
- **Cross-head re-flag** <!-- Added: forge#3641 -->: a finding that an earlier head's review already posted, or that an earlier `FORGE:NOTE_DISPOSITION` list recorded, is not filed or noted a second time after its line moved. The caller builds a prior-findings TSV (file, line-start, line-end, title, prior-disposition, issue#) from trusted prior-head reviewer comments (`Reviewed-SHA` other than the current head) and trusted `FORGE:NOTE_DISPOSITION` records carrying a different `reviewed_sha=`, all read through `scripts/trusted-comments.sh`, and passes it as `--prior-findings`. A finding re-flags a row when it is the same file AND (line IoU >= 0.6 after a best-effort `git diff` hunk-offset shift, OR >= 3 shared title tokens) and becomes `NOTE reflag-filed #N` (the prior row's issue is still open) or `NOTE reflag-note`. Never demoted: HIGH/CRITICAL and unparseable severity (the classifier exits on them before this gate, so they keep blocking via §7B), and any row whose linked issue is closed (a regressed defect is classified normally). Prior rows with no issue link are passed as `note
|
|
2563
|
+
- **Cross-head re-flag** <!-- Added: forge#3641 -->: a finding that an earlier head's review already posted, or that an earlier `FORGE:NOTE_DISPOSITION` list recorded, is not filed or noted a second time after its line moved. The caller builds a prior-findings TSV (file, line-start, line-end, title, prior-disposition, issue#) from trusted prior-head reviewer comments (`Reviewed-SHA` other than the current head) and trusted `FORGE:NOTE_DISPOSITION` records carrying a different `reviewed_sha=`, all read through `scripts/trusted-comments.sh`, and passes it as `--prior-findings`. A finding re-flags a row when it is the same file AND (line IoU >= 0.6 after a best-effort `git diff` hunk-offset shift AND >= 1 shared non-stopword title token, OR >= 3 shared title tokens of which >= 2 are non-stopword; generic stopwords such as `the`, `with`, `before`, `write` never count as topical similarity) and becomes `NOTE reflag-filed #N` (the prior row's issue is still open) or `NOTE reflag-note`. Never demoted: HIGH/CRITICAL and unparseable severity (the classifier exits on them before this gate, so they keep blocking via §7B), and any row whose linked issue is closed (a regressed defect is classified normally). Prior rows with no issue link are passed as `note` (recorded NOTE_DISPOSITION) or `unknown` (reviewer-body finding whose linkage failed; the classifier never demotes a MEDIUM CONFIRMED/LIKELY re-flag against an `unknown` row); an unreadable source only shrinks the TSV, it never blocks.
|
|
2557
2564
|
- **Contract-declared scope** <!-- Added: forge#3447 -->: the trusted, latest FORGE:CONTRACT of `MERGE_ISSUE` lists out-of-scope items as `deferred → #N`, `not-affected:` or `accepted-risk:` (parsed by `scripts/check-contract-scope.sh list`). A finding whose file equals, or sits under, a `deferred` item whose issue is verified becomes `NOTE contract-deferred #N` and is commented on that issue instead of being filed again. A finding under an `accepted-risk` item becomes `NOTE contract-accepted-risk`. A deferred issue is verified only when it is OPEN, is an issue (not a PR), is not `MERGE_ISSUE` itself, and its body carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` and the deferred path, and it is authored by a trusted author (`author_association` OWNER/MEMBER/COLLABORATOR, or `user.type` Bot); an unverified item is dropped from the scope list. Never demoted: HIGH/CRITICAL, `not-affected` items (a contradicting finding means the contract was wrong), safety-exempt findings under either disposition, findings on a file this PR changed, and anything when the contract is missing, untrusted, unparseable or the deferred issue is closed, unverified or unreadable.
|
|
2558
2565
|
|
|
2559
2566
|
```bash
|
|
@@ -2675,17 +2682,17 @@ if [ -z "${TRUSTED_SCRIPT:-}" ]; then
|
|
|
2675
2682
|
fi
|
|
2676
2683
|
# Cross-head re-flag input (#3641). Fail soft: every unreadable source just leaves the TSV smaller, never blocks the review.
|
|
2677
2684
|
# PF_SKIP="<sha or empty>" PF_PRS="<pr numbers>" prior_findings_tsv prints file<TAB>line-start<TAB>line-end<TAB>title<TAB>disposition<TAB>issue# rows
|
|
2678
|
-
# (disposition: filed = linked issue OPEN, filed-closed = linked issue CLOSED, note = no linked issue). Every comment is read through
|
|
2685
|
+
# (disposition: filed = linked issue OPEN, filed-closed = linked issue CLOSED, note = a recorded NOTE_DISPOSITION with no linked issue, unknown = a reviewer-body finding whose issue linkage failed, so the classifier must not let it demote a MEDIUM CONFIRMED/LIKELY re-flag). Every comment is read through
|
|
2679
2686
|
# scripts/trusted-comments.sh, so an untrusted commenter cannot pre-post a finding to suppress a real one. With PF_SKIP set, reviewer
|
|
2680
2687
|
# bodies for that head are skipped and only NOTE_DISPOSITION records carrying a different `reviewed_sha=` count (this head's own record,
|
|
2681
2688
|
# from an earlier run of this same review, must never swallow its findings).
|
|
2682
|
-
pf_split_loc() { # PF_LOC="file[:N[-M]]" -> file<TAB>N<TAB>M (line
|
|
2689
|
+
pf_split_loc() { # PF_LOC="file[:N[-M]]" -> file<TAB>N<TAB>M (`-` sentinel when a line column is absent: a tab IFS read collapses empty fields)
|
|
2683
2690
|
local _loc="$PF_LOC" _file _r _a="" _b=""; _file="$_loc"
|
|
2684
2691
|
case "$_loc" in *:[0-9]*) _r="${_loc##*:}"; case "$_r" in *[!0-9-]*|"") ;; *) _file="${_loc%:*}"; _a="${_r%%-*}"; _b="${_r#*-}"; [ "$_b" != "$_r" ] || _b="$_a" ;; esac ;; esac
|
|
2685
|
-
printf '%s\t%s\t%s\n' "$_file" "$_a" "$_b"
|
|
2692
|
+
printf '%s\t%s\t%s\n' "$_file" "${_a:--}" "${_b:--}"
|
|
2686
2693
|
}
|
|
2687
2694
|
prior_findings_tsv() {
|
|
2688
|
-
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b
|
|
2695
|
+
local pr cj rows issues _id _c _s _loc _t _p _hit _f _a _b _src _dflt
|
|
2689
2696
|
[ -n "${TRUSTED_SCRIPT:-}" ] || return 0
|
|
2690
2697
|
rows=$(mktemp "${_pscr}/prior-rows.XXXXXX"); issues=$(mktemp "${_pscr}/prior-issues.XXXXXX")
|
|
2691
2698
|
for pr in $PF_PRS; do
|
|
@@ -2696,21 +2703,25 @@ prior_findings_tsv() {
|
|
|
2696
2703
|
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or (test("(^|\\n)Reviewed-SHA: " + $skip + "\\r?(\\n|$)") | not)) | scan("<!-- FINDING:([^>]+) -->") | .[0]' 2>/dev/null \
|
|
2697
2704
|
| while IFS='|' read -r _id _c _s _loc _t; do
|
|
2698
2705
|
[ -n "$_t" ] || continue
|
|
2699
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf '%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
2706
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'F\t%s\t%s\n' "$_p" "$(printf '%s' "$_t" | tr '\t' ' ')"
|
|
2700
2707
|
done >> "$rows"
|
|
2701
2708
|
printf '%s' "$cj" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:NOTE_DISPOSITION' 2>/dev/null \
|
|
2702
2709
|
| jq -r --arg skip "${PF_SKIP:-}" 'select($skip == "" or ((capture("^<!-- FORGE:NOTE_DISPOSITION:[^>]*reviewed_sha=(?<s>[0-9a-f]{40,64})") | .s) as $s | $s != $skip)) | split("\n")[] | select(startswith("- "))' 2>/dev/null \
|
|
2703
2710
|
| sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p' \
|
|
2704
2711
|
| while IFS="$(printf '\t')" read -r _loc _t; do
|
|
2705
2712
|
[ "$_loc" != "?" ] || continue
|
|
2706
|
-
_p=$(PF_LOC="$_loc" pf_split_loc); printf '%s\t%s\n' "$_p" "$_t"
|
|
2713
|
+
_p=$(PF_LOC="$_loc" pf_split_loc); printf 'N\t%s\t%s\n' "$_p" "$_t"
|
|
2707
2714
|
done >> "$rows"
|
|
2708
2715
|
gh issue list -R "$REPO" --label review-finding --state all --search "${pr} in:body" --limit 100 --json number,state,title,body 2>/dev/null \
|
|
2709
2716
|
| jq -c '.[]' >> "$issues" 2>/dev/null || true
|
|
2710
2717
|
done
|
|
2711
2718
|
# Linkage: a filed issue whose body names the file and whose title shares >= 3 tokens with the finding (open preferred).
|
|
2712
|
-
|
|
2719
|
+
# Rows carry a leading source column (F = reviewer FINDING marker, N = NOTE_DISPOSITION record), stripped on output. An unlinked F row may
|
|
2720
|
+
# have been filed under a reworded title, so it is `unknown` (never demotes MEDIUM CONFIRMED/LIKELY); an unlinked N row is a true `note`.
|
|
2721
|
+
sort -u "$rows" | while IFS="$(printf '\t')" read -r _src _f _a _b _t; do
|
|
2713
2722
|
[ -n "$_f" ] || continue
|
|
2723
|
+
[ "$_a" != "-" ] || _a=""; [ "$_b" != "-" ] || _b=""
|
|
2724
|
+
case "$_src" in N) _dflt=note ;; *) _dflt=unknown ;; esac
|
|
2714
2725
|
_hit=$(jq -rs --arg f "$_f" --arg t "$_t" 'def toks: ascii_downcase | [scan("[a-z0-9]{3,}")] | unique;
|
|
2715
2726
|
($t | toks) as $a | [ .[] | select((.body // "") | contains($f))
|
|
2716
2727
|
| select((((.title // "") | toks) as $b | [$a[] | select(. as $x | $b | index($x))] | length) >= 3)
|
|
@@ -2719,7 +2730,7 @@ prior_findings_tsv() {
|
|
|
2719
2730
|
case "$_hit" in
|
|
2720
2731
|
open*) printf '%s\t%s\t%s\t%s\tfiled\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2721
2732
|
closed*) printf '%s\t%s\t%s\t%s\tfiled-closed\t%s\n' "$_f" "$_a" "$_b" "$_t" "${_hit#* }" ;;
|
|
2722
|
-
*) printf '%s\t%s\t%s\t%s\
|
|
2733
|
+
*) printf '%s\t%s\t%s\t%s\t%s\t\n' "$_f" "$_a" "$_b" "$_t" "$_dflt" ;;
|
|
2723
2734
|
esac
|
|
2724
2735
|
done
|
|
2725
2736
|
rm -f "$rows" "$issues"
|
|
@@ -2869,8 +2880,23 @@ PR_NUMBER="{PR_NUMBER}"; MERGE_ISSUE="{MERGE_ISSUE}"
|
|
|
2869
2880
|
FINDING_TITLE_DEDUP="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
|
|
2870
2881
|
# Never dedup against the issue this PR fixes (MERGE_ISSUE): it is about to be closed, so a
|
|
2871
2882
|
# finding "matched" to it is tracked nowhere. Field test #3169: SEC-2 matched #3169 itself and was dropped.
|
|
2872
|
-
|
|
2873
|
-
|
|
2883
|
+
# HELPER_SCRIPT resolver (DEDUP_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
2884
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
2885
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
2886
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
2887
|
+
[ -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)"
|
|
2888
|
+
done
|
|
2889
|
+
DEDUP_SCRIPT=""
|
|
2890
|
+
while IFS= read -r _c; do
|
|
2891
|
+
case "$_c" in /*) [ -z "$DEDUP_SCRIPT" ] && [ -f "$_c/scripts/issue-dedup.sh" ] && DEDUP_SCRIPT="$_c/scripts/issue-dedup.sh" ;; esac
|
|
2892
|
+
done <<< "$_tc"
|
|
2893
|
+
# Unresolved helper: "not deduped" (exit 0, empty result) so the line-range check still runs; NEVER exit 1 (which means "duplicate, skip").
|
|
2894
|
+
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
2895
|
+
DEDUP_RESULT=$(bash "$DEDUP_SCRIPT" "$FINDING_TITLE_DEDUP" "$GH_FLAG" ${MERGE_ISSUE:+--exclude "$MERGE_ISSUE"} 2>&1)
|
|
2896
|
+
DEDUP_EXIT=$?
|
|
2897
|
+
else
|
|
2898
|
+
DEDUP_RESULT=""; DEDUP_EXIT=0
|
|
2899
|
+
fi
|
|
2874
2900
|
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
2875
2901
|
echo "DEDUP: Skipping — $DEDUP_RESULT"
|
|
2876
2902
|
# Skip this finding — do NOT create a duplicate issue
|
|
@@ -3681,7 +3707,7 @@ else
|
|
|
3681
3707
|
# forge#2570: the hold is now scoped by the PR's target branch. This PR still transitions
|
|
3682
3708
|
# to workflow:awaiting-merge (clearing needs-human) in BOTH cases below — the transition is
|
|
3683
3709
|
# unchanged; only who acts next differs:
|
|
3684
|
-
# - `main`
|
|
3710
|
+
# - the deploy gate (`main`, the GitHub `default_branch`, or the default branch's committed `branches.default`; never read from the PR worktree) (or an unresolved base — fail closed): held for a HUMAN merge
|
|
3685
3711
|
# decision, because `staging → main` is the genuine human gate no agent performs.
|
|
3686
3712
|
# - any other base (staging, milestone/* — reversible integration branches): NOT held for
|
|
3687
3713
|
# a human. It is parked for remediate.md Phase M7's base-scoped auto-land bar (bot APPROVED
|
|
@@ -3697,15 +3723,38 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
3697
3723
|
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not
|
|
3698
3724
|
# an empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"`
|
|
3699
3725
|
# check so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
3700
|
-
|
|
3701
|
-
|
|
3726
|
+
# forge#3753: trusted resolver only (GitHub default_branch + committed default-branch config); never forge.yaml from $PWD (the PR worktree is author-controlled). Install roots only; unresolved script => strict.
|
|
3727
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
3728
|
+
FORGE_ROOT=""
|
|
3729
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
3730
|
+
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
3731
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
3732
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
3733
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
3734
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
3735
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
3736
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
3737
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
3738
|
+
# 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)
|
|
3739
|
+
_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)"
|
|
3740
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
3741
|
+
# '${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.
|
|
3742
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
3743
|
+
while IFS= read -r _c; do
|
|
3744
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
3745
|
+
done <<< "$_k"
|
|
3746
|
+
fi
|
|
3747
|
+
GATE_SCRIPT=""; [ -n "$FORGE_ROOT" ] && [ -f "$FORGE_ROOT/scripts/resolve-deploy-gate.sh" ] && GATE_SCRIPT="$FORGE_ROOT/scripts/resolve-deploy-gate.sh"
|
|
3748
|
+
if [ -n "$GATE_SCRIPT" ]; then GUARD_GATE=$(bash "$GATE_SCRIPT" "$GUARD_BASE" {GH_REPO} 2>/dev/null || echo true); else GUARD_GATE=true; fi; [ "$GUARD_GATE" = "false" ] || GUARD_GATE=true
|
|
3749
|
+
if [ "$GUARD_GATE" = "false" ]; then
|
|
3750
|
+
# forge#2570: non-default-branch base — defer to remediate.md M7's auto-land, not a human.
|
|
3702
3751
|
# forge#2626: standalone `/review-pr` (no remediate.md M6→M7 chain) would otherwise
|
|
3703
3752
|
# strand this PR at `workflow:awaiting-merge` with no actionable guidance — add a
|
|
3704
3753
|
# manual-merge fallback so the comment is correct in both the chained and standalone cases.
|
|
3705
3754
|
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
3706
|
-
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non
|
|
3755
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non-default-branch base \`${GUARD_BASE}\`. It is parked at \`workflow:awaiting-merge\` for the remediation auto-land bar (\`remediate.md\` Phase M7, forge#2570) — which auto-merges it on the same fast-lane condition the normal \`/work-on\` path uses for this target branch — NOT for a human merge decision. If no remediation \`M7\` follows (e.g. this review was invoked standalone), merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
3707
3756
|
else
|
|
3708
|
-
# forge#2570:
|
|
3757
|
+
# forge#2570: default branch / deploy gate (or unresolved base — fail closed to the human gate).
|
|
3709
3758
|
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check. This PR targets the deploy gate (\`${GUARD_BASE:-unresolved base}\`), so it is held at \`workflow:awaiting-merge\` for a human merge decision — \`staging → main\` is the genuine human gate. Merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
3710
3759
|
fi
|
|
3711
3760
|
# Shell state does not persist between Bash calls and `resolve_script` is not defined in this spec:
|
|
@@ -116,7 +116,21 @@ Extract from contract:
|
|
|
116
116
|
| Refactor / Maintenance | Implement directly following contract deliverables |
|
|
117
117
|
| Investigation | Spawn research agents, create GitHub issues for findings, skip to I5 |
|
|
118
118
|
|
|
119
|
-
**Investigation task special case**: Research deeply, create GitHub issues for each finding using the Pipeline Issue Template (see `commands/issue.md` § "Pipeline Issue Template"). Each issue MUST include `## Problem`, `## Affected Files`, and `## Acceptance Criteria`. Create each issue via the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") — `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"...\" --body-file <path> --label ...")` — instead of calling the raw issue-creation command directly; this gets dedup and body validation for free. Post
|
|
119
|
+
**Investigation task special case**: Research deeply, create GitHub issues for each finding using the Pipeline Issue Template (see `commands/issue.md` § "Pipeline Issue Template"). Each issue MUST include `## Problem`, `## Affected Files`, and `## Acceptance Criteria`. Create each issue via the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") — `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"...\" --body-file <path> --label ...")` — instead of calling the raw issue-creation command directly; this gets dedup and body validation for free. Post the durable deliverables comment, remove the stale `workflow:building` label, close the original issue, and print `IMPLEMENT_RESULT: status: INVESTIGATION_COMPLETE` as the final reply (the build skill passes this up and the router skips review). <!-- Added: forge#2090 -->
|
|
120
|
+
|
|
121
|
+
The deliverables comment MUST be posted BEFORE the issue is closed, MUST start with the marker line below as its first line, and lists each created issue. The engine reads this marker (not the free-text result) to tell a legitimate investigation exit from a crashed build, since an investigation posts no `FORGE:BUILDER:COMPLETE` and has no commits. <!-- Added: forge#3534 -->
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
DELIVERABLES_BODY="<!-- FORGE:INVESTIGATION:DELIVERABLES -->
|
|
125
|
+
## Investigation Deliverables
|
|
126
|
+
|
|
127
|
+
- #{CREATED_ISSUE_1}: {title}
|
|
128
|
+
- #{CREATED_ISSUE_2}: {title}"
|
|
129
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$DELIVERABLES_BODY" # allowlist:check-command-side-effects
|
|
130
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:building" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Resume: if a `<!-- FORGE:INVESTIGATION:DELIVERABLES -->` comment already exists on the issue, do not research or create issues again; ensure the label cleanup and close are done and print `INVESTIGATION_COMPLETE`.
|
|
120
134
|
|
|
121
135
|
---
|
|
122
136
|
|
|
@@ -23,7 +23,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
23
23
|
**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 (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
|
|
24
24
|
**NEVER use plan mode (EnterPlanMode).**
|
|
25
25
|
|
|
26
|
-
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the exceptions are the B0 size-gate resume branch, which enters at B5.5, and the B0 gate-resume branch, which enters at B6.5, each with its state verified). Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
27
27
|
|
|
28
28
|
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
29
29
|
|
|
@@ -172,7 +172,10 @@ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
**Resume check**:
|
|
175
|
-
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment →
|
|
175
|
+
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted, current-cycle (after the newest `FORGE:CYCLE`) comment that starts with `FORGE:ACCEPTANCE_GATE` or `FORGE:BUILD_BLOCKED`, through the same anchored, trust-filtered lookup as the size-gate resume below (script resolution block verbatim; use `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
176
|
+
- Lookup failed (`trusted-comments.sh` unresolved or `gh` error) → Blocked exit with blocker `gate-lookup-unavailable: could not read FORGE:ACCEPTANCE_GATE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no gate marker".
|
|
177
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:FAILED`, `FORGE:ACCEPTANCE_GATE:BLOCKED`, or a `FORGE:BUILD_BLOCKED` comment → the gate is blocked and the engine fails closed on it, so returning ALREADY_DONE would deadlock every re-run. Derive BRANCH/WORKTREE_PATH/PR_BASE as in B1A/B1C (without creating anything), verify the branch and worktree exist and that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH (missing or mismatched → Blocked exit `worktree-missing: cannot re-enter B6.5`; never recreate silently), recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction, then skip B0.5-B6 and re-enter at **Phase B6.5**, which posts a fresh PASSED/FAILED/BLOCKED verdict (the one-repair `FORGE:ACCEPTANCE_REPAIR` bound still applies).
|
|
178
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED`, or no gate marker exists → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
176
179
|
- **Size-gate resume** (checked BEFORE the partial-BUILDER rule below; first match wins) <!-- Added: forge#3463 -->: if a `<!-- FORGE:BUILDER -->` comment exists without `<!-- FORGE:BUILDER:COMPLETE -->` AND the latest trusted `FORGE:DIFF_SIZE` comment has `result: OVER`, the build stopped at a deliberate B5.5 exit (`NEEDS_DECOMPOSE` or the decompose-loop-guard Blocked exit), not in a crash. Keep the BUILDER comment, do not re-run B2-B5, and re-enter at **Phase B5.5**. Read the marker through the same anchored, trust-filtered lookup B5.5 uses (script resolution block verbatim):
|
|
177
180
|
```bash
|
|
178
181
|
# <Script resolution block from above goes here, verbatim>
|
|
@@ -607,7 +610,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
607
610
|
**After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
|
|
608
611
|
- `status: COMPLETE` → continue to B5.5
|
|
609
612
|
- `status: ALREADY_DONE` → continue to B5.5 (size-check what's already there)
|
|
610
|
-
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5)
|
|
613
|
+
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; verify a `<!-- FORGE:INVESTIGATION:DELIVERABLES -->` comment exists on the issue (the implement child posts it and removes `workflow:building`; if it is missing, post it listing the created issues and remove the label), then print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5). The engine keys off that marker, not the result text. <!-- Added: forge#3534 -->
|
|
611
614
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
612
615
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
613
616
|
- Returned running/backgrounded/empty (no `IMPLEMENT_RESULT:`) → do not end the turn to wait; re-read the `FORGE:BUILDER` comment and the worktree state; if `FORGE:BUILDER:COMPLETE` is present consume it, and if the worktree shows in-flight edits or a fresh commit treat the child as possibly live; before re-invoking, apply the bounded re-invoke rule (at most 2, then `child-stalled: implement`)
|
|
@@ -750,7 +753,7 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
750
753
|
|
|
751
754
|
## Phase B6.5: Acceptance Gate (MANDATORY — cannot be silently skipped) <!-- Added: forge#1315 -->
|
|
752
755
|
|
|
753
|
-
**Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory. Run the checks from the worktree (`cd "{WORKTREE_PATH}"`) so relative targets resolve against the built code.
|
|
756
|
+
**Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory. Run the checks from the worktree (`cd "{WORKTREE_PATH}"`) so relative targets resolve against the built code. This phase may also be entered directly from the B0 gate resume (BRANCH/WORKTREE_PATH/PR_BASE re-derived there) when a prior run left a blocked gate.
|
|
754
757
|
|
|
755
758
|
**Read acceptance spec from FORGE:INVESTIGATOR comment**:
|
|
756
759
|
|
|
@@ -605,8 +605,9 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
605
605
|
- Per affected file: a new file counts about its full size; an edit counts about its hunks (added plus deleted lines). Add roughly 1:1 test lines for changed behavior. Skip lockfiles, generated files and fixtures (the gate excludes them).
|
|
606
606
|
- `INDEPENDENT_SEAMS`: the number of independently buildable pieces. For a batch issue (several review findings or checklist members) this is the member count; otherwise count distinct directory/phase seams. Minimum 1.
|
|
607
607
|
- Resolve config (defaults when absent): `THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)`, `FACTOR=$(yq '.build.diff_size.estimate_factor // 1.2' forge.yaml 2>/dev/null || echo 1.2)`, `SEAM_MIN=$(yq '.build.diff_size.batch_seam_min // 3' forge.yaml 2>/dev/null || echo 3)`.
|
|
608
|
-
- Rule: when `build.diff_size.threshold` is `0` the rule is skipped (emit `GATE_THRESHOLD: 0`). Otherwise the Decomposition Assessment MUST be `**YES**` when `ESTIMATED_DIFF_LINES` > threshold x
|
|
609
|
-
-
|
|
608
|
+
- Rule: when `build.diff_size.threshold` is `0` the rule is skipped (emit `GATE_THRESHOLD: 0`). Otherwise the Decomposition Assessment MUST be `**YES**` when `ESTIMATED_DIFF_LINES` > threshold x `FACTOR` (`estimate_factor`, default 1.2), OR when `INDEPENDENT_SEAMS` >= `batch_seam_min` and `ESTIMATED_DIFF_LINES` > threshold / 2 (not well under the gate). Propose one sub-issue per seam. A batch that is well under the gate (estimate at or under half the threshold) may stay `**NO**`.
|
|
609
|
+
- **Exemption** <!-- Added: forge#3676 -->: emit `**NO**` (state the guard in the reason; still emit the numeric fields) instead of `**YES**` when the issue is already a sub-issue (its body has a `**Parent**: #N` line), a `FORGE:DECOMPOSED` comment exists, or a justified `FORGE:SIZE_OVERRIDE` exists. Decompose Guard 1 refuses these issues, so a YES would stall the run; the over-threshold YES rule applies only otherwise. The engine applies the same guard when it reads the verdict.
|
|
610
|
+
- Emit the numbers in the comment as the own-line fields in Phase 1C. A pure estimate is a heuristic; state it in the reason, do not pad it.
|
|
610
611
|
8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
611
612
|
|
|
612
613
|
---
|
|
@@ -712,6 +713,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
712
713
|
ESTIMATED_DIFF_LINES: {N}
|
|
713
714
|
INDEPENDENT_SEAMS: {N}
|
|
714
715
|
GATE_THRESHOLD: {N — build.diff_size.threshold; 0 when the gate is disabled}
|
|
716
|
+
ESTIMATE_FACTOR: {FACTOR — resolved build.diff_size.estimate_factor, positive number, decimal allowed; 1.2 by default}
|
|
715
717
|
<!-- DECOMPOSE:{YES|NO} -->
|
|
716
718
|
|
|
717
719
|
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
@@ -748,7 +750,7 @@ ${INVESTIGATION_SENTINEL}"
|
|
|
748
750
|
|
|
749
751
|
**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 -->
|
|
750
752
|
|
|
751
|
-
**Size-estimate fields (MANDATORY when Verdict is not INVALID)** <!-- Added: forge#3573 -->: `ESTIMATED_DIFF_LINES: N`, `INDEPENDENT_SEAMS: N` and `GATE_THRESHOLD: N` are each alone on their own line (whole numbers, no prose or backticks on the line), INSIDE `### Decomposition Assessment` after the verdict line and sub-issue list and before the `<!-- DECOMPOSE:.. -->` marker. The engine reads them with full-line anchors as a safety net: an estimate over `GATE_THRESHOLD` x 1.2 routes to decompose even when the verdict says NO, so the verdict and the numbers must agree. Never quote these field lines elsewhere in the comment.
|
|
753
|
+
**Size-estimate fields (MANDATORY when Verdict is not INVALID)** <!-- Added: forge#3573 -->: `ESTIMATED_DIFF_LINES: N`, `INDEPENDENT_SEAMS: N` and `GATE_THRESHOLD: N` are each alone on their own line (whole numbers, no prose or backticks on the line), as is `ESTIMATE_FACTOR: F` (the resolved `FACTOR`, a positive number, decimal allowed), INSIDE `### Decomposition Assessment` after the verdict line and sub-issue list and before the `<!-- DECOMPOSE:.. -->` marker. The engine reads them with full-line anchors as a safety net: an estimate over `GATE_THRESHOLD` x `ESTIMATE_FACTOR` (1.2 when the field is absent or invalid) routes to decompose even when the verdict says NO, so the verdict and the numbers must agree. Never quote these field lines elsewhere in the comment.
|
|
752
754
|
|
|
753
755
|
**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 -->
|
|
754
756
|
|