forgedock 1.7.0 → 1.8.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/README.md +2 -1
- package/bin/engine/admission.mjs +242 -7
- package/bin/engine/admission.test.mjs +183 -2
- package/bin/engine/phases.mjs +31 -22
- package/bin/engine/resolve.mjs +8 -1
- package/bin/engine/resolve.test.mjs +5 -0
- package/bin/engine-cli.mjs +2 -0
- package/bin/engine.mjs +26 -17
- package/bin/forgedock.mjs +124 -48
- package/bin/hooks/pre-tool-use.mjs +573 -2
- package/bin/journey.mjs +11 -7
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +652 -92
- package/bin/orchestrate-preflight.mjs +434 -0
- package/bin/runner.mjs +77 -9
- package/commands/audit-agents.md +75 -26
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +10 -5
- package/commands/issue.md +95 -35
- package/commands/orchestrate/phase-1-resolve.md +70 -32
- package/commands/orchestrate/phase-3-dependency.md +167 -22
- package/commands/orchestrate/phase-4-execution.md +691 -62
- package/commands/orchestrate/phase-5-cleanup.md +5 -11
- package/commands/orchestrate/phase-6-report.md +27 -1
- package/commands/orchestrate/safety.md +1 -1
- package/commands/orchestrate.md +39 -1
- package/commands/quality-gate.md +30 -10
- package/commands/review-pr-agents/protocols.md +13 -0
- package/commands/review-pr-agents.md +8 -0
- package/commands/review-pr-staging.md +117 -17
- package/commands/review-pr.md +95 -23
- package/commands/work-on/build/validate.md +25 -4
- package/commands/work-on/close.md +40 -10
- package/commands/work-on/decompose.md +33 -1
- package/commands/work-on/investigate.md +33 -0
- package/commands/work-on/remediate.md +40 -5
- package/commands/work-on/review.md +27 -0
- package/commands/work-on.md +48 -5
- package/docs/CONFIG.md +51 -15
- package/docs/OPENCODE.md +128 -25
- package/docs/spec/review-protocol.md +7 -0
- package/package.json +1 -1
- package/scripts/check-admission-parity.mjs +1 -0
- package/scripts/check-command-side-effects.sh +14 -13
- package/scripts/check-shell-portability.sh +36 -0
- package/scripts/check-spec-markers.sh +21 -11
- package/scripts/checkbox-sections.test.sh +67 -0
- package/scripts/extract-affected-files.sh +85 -5
- package/scripts/extract-affected-files.test.sh +154 -2
package/commands/review-pr.md
CHANGED
|
@@ -33,9 +33,28 @@ allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch, Skill
|
|
|
33
33
|
|
|
34
34
|
This spec dispatches domain review agents via a sub-agent-spawning tool. Different runtimes expose different tools for this — resolve deterministically, once per invocation, and use the same tool for every dispatch call in this run:
|
|
35
35
|
|
|
36
|
+
### OpenCode Runtime Override
|
|
37
|
+
|
|
38
|
+
When the workflow is running under OpenCode, resolve the native tool before applying the Claude-specific availability order above:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
IS_OPENCODE_RUNTIME=false
|
|
42
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
43
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
44
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
45
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
46
|
+
IS_OPENCODE_RUNTIME=true
|
|
47
|
+
DISPATCH_TOOL=task
|
|
48
|
+
DISPATCH_SUBAGENT_TYPE=general
|
|
49
|
+
fi
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When `IS_OPENCODE_RUNTIME=true`, lowercase native `task` is the preferred isolated dispatch tool. Do not enter the `Neither tool is available` branch merely because Claude's literal `Task` and `Agent` names are absent. Every native task call must use a top-level `subagent_type`: `general` for implementation/review work and `explore` for read-only discovery (Claude `general-purpose` and `codebase-explorer` map to those values). Review tasks are foreground: wait for their completed result before synthesis. If the native `task` capability itself is absent from the current tool registry, use the existing hard-stop path and post `FORGE:REVIEW_BLOCKED`; never substitute inline review.
|
|
53
|
+
|
|
36
54
|
1. **If `Task` is available in the current environment**: set `{DISPATCH_TOOL} = Task`. This is the preferred tool — tightest `allowed-tools` scoping.
|
|
37
55
|
2. **Else if `Agent` is available**: set `{DISPATCH_TOOL} = Agent`. This is the documented fallback, not a degraded path — use it exactly as you would `Task`: one call per selected domain agent, same prompt template, `subagent_type: "general-purpose"` (or the closest equivalent the environment offers), same requirement that each agent posts its own findings directly to the PR via `gh pr comment`. Isolation and fresh-context review are preserved either way.
|
|
38
56
|
3. **Neither tool is available**: this is a genuine setup defect, not a routing decision — HARD STOP, post a PR/issue comment explaining that no sub-agent dispatch tool is available, add `needs-human`, and exit without posting a verdict. Do NOT fall back to reviewing inline in the orchestrator's own context — inline self-review is strictly weaker than an isolated fresh-context reviewer and is never a substitute for a missing dispatch tool.
|
|
57
|
+
4. **Dispatch pool exhausted or dispatch call fails**: this is distinct from tool absence. If any selected reviewer cannot be launched because the runtime reports a sub-agent/session/pool limit (or any dispatch call fails), HARD STOP immediately. Do not review that domain inline, do not silently reduce the panel, and do not merge. Mark the PR `review-degraded`, add `needs-human` to the linked issue, post `<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->` with the selected and completed reviewer counts, then exit without a `FORGE:REVIEW` verdict. A later fresh session must re-run the full selected panel.
|
|
39
58
|
|
|
40
59
|
**Do not halt to ask the operator which tool to use.** Steps 1–2 are deterministic and fully resolve the common case; only step 3 (both absent) requires a stop, and even then the action is HARD STOP + `needs-human`, not a question back to the operator.
|
|
41
60
|
|
|
@@ -1520,9 +1539,31 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
|
|
|
1520
1539
|
6. Substitute code index slice: `[INDEX_SLICE]` → the matching `$INDEX_SLICE_{DOMAIN}` variable for this agent (e.g., `$INDEX_SLICE_AUTH` for the auth agent). Agents MUST query index data first; fall back to grep only when index slice is empty or unavailable.
|
|
1521
1540
|
7. Substitute per-agent diff slice: `[DOMAIN_DIFF_SLICE]` → the matching `$DIFF_SLICE_*` variable (e.g., `$DIFF_SLICE_AUTH` for the auth agent, `$DIFF_SLICE_SECURITY` for the security agent). This replaces any `gh pr diff [PR_NUMBER]` call inside the agent template — the agent works from the pre-computed slice, not the full changeset.
|
|
1522
1541
|
8. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
|
|
1523
|
-
9. Launch via the resolved `{DISPATCH_TOOL}` (see Sub-Agent Dispatch Tool Resolution above) with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited)
|
|
1542
|
+
9. Launch via the resolved `{DISPATCH_TOOL}` (see Sub-Agent Dispatch Tool Resolution above) with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited). Under OpenCode, emit a top-level `subagent_type: "general"` or `"explore"` in the native `task` argument object and use `background: false` for each reviewer.
|
|
1543
|
+
|
|
1544
|
+
**CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. Each agent must persist its finalized body before posting it with `gh pr comment --body-file`, include `<!-- FORGE:REVIEW-AGENT:{lowercase-domain} -->`, and return its verdict and findings to the orchestrator independently of GitHub delivery.
|
|
1524
1545
|
|
|
1525
|
-
**
|
|
1546
|
+
**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 create the managed label if necessary, label the PR `review-degraded`, add `needs-human` to the linked issue, post `FORGE:GATE_FAILURE`, and exit without a verdict. After all foreground reviewers return, independently compare their posted `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.
|
|
1547
|
+
|
|
1548
|
+
```bash
|
|
1549
|
+
SELECTED_AGENT_COUNT=$(echo "$SELECTED_AGENTS" | tr ' ' '\n' | grep -c '.')
|
|
1550
|
+
ACTUAL_AGENT_COUNT=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
1551
|
+
--jq '[.[].body | scan("<!-- FORGE:REVIEW-AGENT:([a-z-]+) -->") | .[0]] | unique | length' 2>/dev/null || echo 0)
|
|
1552
|
+
|
|
1553
|
+
if [ "$DISPATCH_FAILED" = "true" ] || [ "$ACTUAL_AGENT_COUNT" -lt "$SELECTED_AGENT_COUNT" ]; then
|
|
1554
|
+
gh label create "review-degraded" --color "E4E669" --description "PR review panel was incomplete; re-review required before deployment. Managed by ForgeDock." --force -R "$REPO" 2>/dev/null || true
|
|
1555
|
+
gh pr edit "$PR_NUMBER" -R "$REPO" --add-label "review-degraded" --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1556
|
+
gh pr comment "$PR_NUMBER" -R "$REPO" --body "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
1557
|
+
## Review Blocked: Incomplete Isolated Review Panel
|
|
1558
|
+
|
|
1559
|
+
**Selected isolated reviewers**: ${SELECTED_AGENT_COUNT}
|
|
1560
|
+
**Completed isolated reviewers**: ${ACTUAL_AGENT_COUNT}
|
|
1561
|
+
|
|
1562
|
+
At least one reviewer could not be dispatched or complete, commonly because the per-session sub-agent pool was exhausted. No inline substitution was performed. Re-run the full panel in a fresh session before merging."
|
|
1563
|
+
[ -n "${MERGE_ISSUE:-}" ] && gh issue edit "$MERGE_ISSUE" {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1564
|
+
exit 1
|
|
1565
|
+
fi
|
|
1566
|
+
```
|
|
1526
1567
|
|
|
1527
1568
|
#### Domain Diff Slicing
|
|
1528
1569
|
|
|
@@ -1551,9 +1592,29 @@ When substituting `[FILE_LIST]` in each agent's template:
|
|
|
1551
1592
|
```bash
|
|
1552
1593
|
gh pr view $ARGUMENTS --json comments --jq '.comments | length'
|
|
1553
1594
|
gh api repos/{owner}/{repo}/issues/$ARGUMENTS/comments --jq '.[-10:] | .[].body[:100]'
|
|
1595
|
+
|
|
1596
|
+
# Every dispatched agent must have delivered its own persisted review to GitHub.
|
|
1597
|
+
# Do not infer a clean review from a missing comment: its return text may contain
|
|
1598
|
+
# findings that could not be posted because GitHub writes were throttled.
|
|
1599
|
+
MISSING_AGENT_COMMENTS=""
|
|
1600
|
+
for AGENT in $SELECTED_AGENTS; do
|
|
1601
|
+
AGENT_DOMAIN=$(printf '%s' "$AGENT" | tr '[:upper:]' '[:lower:]')
|
|
1602
|
+
AGENT_COMMENT_COUNT=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
1603
|
+
--jq "[.[] | select(.body | contains(\"<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->\"))] | length" \
|
|
1604
|
+
2>/dev/null || printf '0')
|
|
1605
|
+
if [ "${AGENT_COMMENT_COUNT:-0}" -lt 1 ]; then
|
|
1606
|
+
MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}"
|
|
1607
|
+
fi
|
|
1608
|
+
done
|
|
1609
|
+
|
|
1610
|
+
if [ -n "$MISSING_AGENT_COMMENTS" ]; then
|
|
1611
|
+
echo "REVIEW DELIVERY FAILURE: missing findings comment(s) for:${MISSING_AGENT_COMMENTS}"
|
|
1612
|
+
echo "Use each agent's returned verdict, findings, and durable body path to recover the review."
|
|
1613
|
+
exit 1
|
|
1614
|
+
fi
|
|
1554
1615
|
```
|
|
1555
1616
|
|
|
1556
|
-
**Do NOT proceed until ALL launched agent comments are visible on the PR.**
|
|
1617
|
+
**Do NOT proceed until ALL launched agent comments are visible on the PR.** Under OpenCode, each foreground `task(..., background: false)` returns only when that reviewer is complete; verify its corresponding structured PR comment before continuing. A missing comment is an explicit delivery failure: do not synthesize a verdict or treat it as a clean result.
|
|
1557
1618
|
|
|
1558
1619
|
---
|
|
1559
1620
|
|
|
@@ -1734,7 +1795,11 @@ FINDING_ISSUE_TITLE="fix: [summary] (review finding — PR #${PR_NUMBER})"
|
|
|
1734
1795
|
# so the raw title stays readable if it round-trips through any other
|
|
1735
1796
|
# eval-based consumer.
|
|
1736
1797
|
FINDING_ISSUE_TITLE=$(printf '%s' "$FINDING_ISSUE_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
1737
|
-
|
|
1798
|
+
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
1799
|
+
REVIEW_AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-reviewer}-$$}"
|
|
1800
|
+
mkdir -p "$SCRATCHPAD"
|
|
1801
|
+
FINDING_BODY_MARKER="FORGE:BODY-INTEGRITY:${PR_NUMBER}_review_${REVIEW_AGENT_TOKEN}"
|
|
1802
|
+
FINDING_ISSUE_BODY_FILE=$(mktemp "$SCRATCHPAD/${PR_NUMBER}_review_${REVIEW_AGENT_TOKEN}.XXXXXX.md")
|
|
1738
1803
|
cat <<'ISSUE_EOF' > "$FINDING_ISSUE_BODY_FILE"
|
|
1739
1804
|
## Problem
|
|
1740
1805
|
|
|
@@ -1754,9 +1819,11 @@ cat <<'ISSUE_EOF' > "$FINDING_ISSUE_BODY_FILE"
|
|
|
1754
1819
|
**Prevention**: [one sentence — what the builder must do to avoid this class of bug]
|
|
1755
1820
|
|
|
1756
1821
|
<!-- FORGE:PATTERN: [pattern-slug] -->
|
|
1822
|
+
<!-- FORGE:CLASS: [pattern-slug] -->
|
|
1757
1823
|
<!-- This machine-readable tag is used by pipeline-health Phase 4A to count pattern recurrences.
|
|
1758
|
-
|
|
1759
|
-
|
|
1824
|
+
When this slug appears on 3+ findings, a check-promotion issue is automatically filed.
|
|
1825
|
+
Keep the slug consistent across all findings for the same defect class. FORGE:CLASS is
|
|
1826
|
+
consumed by orchestrate's P3 batching rule; use a lowercase hyphenated slug. --> <!-- Added: forge#1331, forge#2858 -->
|
|
1760
1827
|
|
|
1761
1828
|
## Affected Files
|
|
1762
1829
|
|
|
@@ -1802,6 +1869,7 @@ Files that need changes:
|
|
|
1802
1869
|
- [ ] Reproduce or construct proof-of-concept
|
|
1803
1870
|
[BATCHABLE_ANNOTATION]
|
|
1804
1871
|
ISSUE_EOF
|
|
1872
|
+
printf '\n<!-- %s -->\n' "$FINDING_BODY_MARKER" >> "$FINDING_ISSUE_BODY_FILE"
|
|
1805
1873
|
|
|
1806
1874
|
# FINDING_SEVERITY is extracted from the finding's own **Severity** body field
|
|
1807
1875
|
# (set above in the heredoc) — example assignment shown here for clarity, same
|
|
@@ -1826,19 +1894,22 @@ if [ "$FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
|
1826
1894
|
else
|
|
1827
1895
|
|
|
1828
1896
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
1829
|
-
Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG}")
|
|
1897
|
+
ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
|
|
1898
|
+
# /issue re-reads the created issue and hard-fails unless this exact marker is present.
|
|
1830
1899
|
rm -f "$FINDING_ISSUE_BODY_FILE"
|
|
1831
1900
|
|
|
1832
|
-
# /issue
|
|
1833
|
-
#
|
|
1834
|
-
#
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
ISSUE_NUM
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1901
|
+
# /issue succeeds only after its API create-token read-back (Phase 4B). Its
|
|
1902
|
+
# explicit result marker distinguishes a verified create from an intentional
|
|
1903
|
+
# dedup STOP; never use title search to mask a swallowed 403.
|
|
1904
|
+
ISSUE_NUM=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:CREATED number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1905
|
+
DEDUP_NUMBER=$(printf '%s\n' "$ISSUE_SKILL_OUTPUT" | sed -n 's/.*ISSUE_CREATE_RESULT:DEDUP number=\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
1906
|
+
if [ -n "$DEDUP_NUMBER" ]; then
|
|
1907
|
+
ISSUE_NUM="$DEDUP_NUMBER"
|
|
1908
|
+
echo "Review finding deduped against existing issue #${ISSUE_NUM}."
|
|
1909
|
+
elif [ -z "$ISSUE_NUM" ]; then
|
|
1910
|
+
echo "ERROR: /issue did not report a verified review-finding number; stopping review instead of silently dropping the finding." >&2
|
|
1911
|
+
exit 1
|
|
1912
|
+
fi
|
|
1842
1913
|
fi
|
|
1843
1914
|
```
|
|
1844
1915
|
|
|
@@ -2131,9 +2202,10 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
|
|
|
2131
2202
|
else
|
|
2132
2203
|
|
|
2133
2204
|
# Previously-escalated re-review guard <!-- Added: forge#1810; base-scoped: forge#2570 -->
|
|
2134
|
-
# If the linked issue currently carries needs-human,
|
|
2135
|
-
#
|
|
2136
|
-
#
|
|
2205
|
+
# If the linked issue currently carries needs-human, or remediation has posted its
|
|
2206
|
+
# in-progress marker, this PR was escalated at some earlier point
|
|
2207
|
+
# (VERDICT/purpose-regression/calibration/trust/mergeability) and has now been
|
|
2208
|
+
# remediated + re-reviewed back to a clean, mergeable APPROVED state above.
|
|
2137
2209
|
#
|
|
2138
2210
|
# forge#2570: the hold is now scoped by the PR's target branch. This PR still transitions
|
|
2139
2211
|
# to workflow:awaiting-merge (clearing needs-human) in BOTH cases below — the transition is
|
|
@@ -2146,8 +2218,8 @@ else
|
|
|
2146
2218
|
# uses for the same target branch — no manual click. This reconciles the asymmetry where a
|
|
2147
2219
|
# bot-only re-review (authorAssociation=NONE) could never satisfy the strict ≥2 verified-
|
|
2148
2220
|
# human bar and so stranded staging PRs the fast lane would auto-merge.
|
|
2149
|
-
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels \
|
|
2150
|
-
--jq '[.labels[].name
|
|
2221
|
+
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels,comments \
|
|
2222
|
+
--jq '([.labels[].name | . == "needs-human"] + [.comments[].body | contains("FORGE:REMEDIATION")]) | any' 2>/dev/null || echo "false")
|
|
2151
2223
|
GUARD_BASE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2152
2224
|
|
|
2153
2225
|
if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
@@ -2355,7 +2427,7 @@ ACTUAL_AGENT_DOMAINS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
|
2355
2427
|
ACTUAL_AGENT_COUNT=$(echo "$ACTUAL_AGENT_DOMAINS" | tr ',' '\n' | grep -c '.' 2>/dev/null)
|
|
2356
2428
|
```
|
|
2357
2429
|
|
|
2358
|
-
Substitute `ACTUAL_AGENT_COUNT`/`ACTUAL_AGENT_DOMAINS` into the summary's `**Agents**: [N] ([names])` field below — do NOT substitute a manually-counted or remembered figure.
|
|
2430
|
+
Substitute `ACTUAL_AGENT_COUNT`/`ACTUAL_AGENT_DOMAINS` into the summary's `**Agents**: [N] ([names])` field below — do NOT substitute a manually-counted or remembered figure. Compare it to `SELECTED_AGENT_COUNT` from Phase 3C. If it is smaller, the panel is degraded: the Phase 3C hard-stop path must already have labelled the PR `review-degraded`, added `needs-human`, and exited without a verdict. Never summarize, approve, or merge a partial panel. `ACTUAL_AGENT_COUNT=0` is the same hard stop, not a solo/inline review mode.
|
|
2359
2431
|
|
|
2360
2432
|
```bash
|
|
2361
2433
|
gh pr comment $ARGUMENTS --body "$(cat <<'EOF'
|
|
@@ -109,6 +109,8 @@ if iteration == max_iterations AND result != PASS AND blocker_findings not empty
|
|
|
109
109
|
Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
**Timeout handling**: The quality-gate's executable checks carry their own explicit timeouts; a `QUALITY-GATE-TIMEOUT` result is a failed gate iteration. Record it as HIGH, do not apply speculative fixes, and proceed to V1-FAIL immediately (post the failure comment, add `needs-human`, return `GATE_PASSED: false`). Do not retry a timed-out check. A wall-time timeout for the in-context `Skill(...)` invocation itself requires native runtime support and is not claimed by this workflow.
|
|
113
|
+
|
|
112
114
|
**Rules**:
|
|
113
115
|
- Re-run after EVERY fix pass — never trust that fixes resolved findings without verification
|
|
114
116
|
- Each iteration re-scans ALL changed files — fixes can introduce new issues
|
|
@@ -138,6 +140,24 @@ Run after quality gate passes. All tool commands are read from `forge.yaml → v
|
|
|
138
140
|
**Track skipped checks** — initialize before any check runs:
|
|
139
141
|
```bash
|
|
140
142
|
SKIPPED_CHECKS=""
|
|
143
|
+
|
|
144
|
+
# Verification commands are project configuration; bound each one so a stalled
|
|
145
|
+
# formatter, typechecker, or build cannot block validation indefinitely.
|
|
146
|
+
# Set FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS to override the 120-second default.
|
|
147
|
+
run_verification_command() {
|
|
148
|
+
local label="$1" command="$2"
|
|
149
|
+
local timeout_seconds="${FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS:-120}"
|
|
150
|
+
if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then
|
|
151
|
+
echo "VERIFY-CONFIG | HIGH | timeout | FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS must be a positive integer (got '$timeout_seconds')"
|
|
152
|
+
return 2
|
|
153
|
+
fi
|
|
154
|
+
timeout "$timeout_seconds" bash -c "$command" 2>&1
|
|
155
|
+
local command_exit=$?
|
|
156
|
+
if [ "$command_exit" -eq 124 ]; then
|
|
157
|
+
echo "VERIFY-TIMEOUT | HIGH | $label | timed out after ${timeout_seconds}s"
|
|
158
|
+
fi
|
|
159
|
+
return "$command_exit"
|
|
160
|
+
}
|
|
141
161
|
```
|
|
142
162
|
|
|
143
163
|
**Python**:
|
|
@@ -146,7 +166,7 @@ cd {WORKTREE_PATH}
|
|
|
146
166
|
|
|
147
167
|
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
148
168
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
149
|
-
|
|
169
|
+
run_verification_command "python.format" "$PYTHON_FORMAT"
|
|
150
170
|
else
|
|
151
171
|
echo "SKIPPED — python.format not configured in verification.commands"
|
|
152
172
|
SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }python.format"
|
|
@@ -166,18 +186,19 @@ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml
|
|
|
166
186
|
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
167
187
|
|
|
168
188
|
if [ -n "$TS_FORMAT" ]; then
|
|
169
|
-
|
|
189
|
+
run_verification_command "typescript.format" "$TS_FORMAT"
|
|
170
190
|
else
|
|
171
191
|
echo "SKIPPED — typescript.format not configured in verification.commands"
|
|
172
192
|
SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }typescript.format"
|
|
173
193
|
fi
|
|
174
194
|
|
|
175
195
|
if [ -n "$TS_TYPECHECK" ]; then
|
|
176
|
-
|
|
196
|
+
run_verification_command "typescript.typecheck" "$TS_TYPECHECK"
|
|
177
197
|
TS_EXIT=$?
|
|
178
198
|
elif [ -n "$TS_BUILD" ]; then
|
|
179
|
-
|
|
199
|
+
TS_OUTPUT=$(run_verification_command "typescript.build" "$TS_BUILD")
|
|
180
200
|
TS_EXIT=$?
|
|
201
|
+
echo "$TS_OUTPUT" | tail -30
|
|
181
202
|
else
|
|
182
203
|
echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
|
|
183
204
|
SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }typescript.typecheck/build"
|
|
@@ -139,7 +139,7 @@ The `INVARIANT_ANOMALIES` variable is read in Phase C4.5 (trajectory post) and w
|
|
|
139
139
|
BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
140
140
|
|
|
141
141
|
# Count remaining unchecked items BEFORE any edit
|
|
142
|
-
REMAINING_BEFORE=$(
|
|
142
|
+
REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
**If `REMAINING_BEFORE == 0`** (no unchecked items): skip body edit — all items already checked, proceed to add PR reference only:
|
|
@@ -149,24 +149,54 @@ gh issue edit {NUMBER} {GH_FLAG} --body "{UPDATED_BODY}"
|
|
|
149
149
|
REMAINING_AFTER=0
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
**If `REMAINING_BEFORE > 0`**: check whether ANY
|
|
152
|
+
**If `REMAINING_BEFORE > 0`**: check whether ANY unchecked GFM task items will still remain after a full check-off — i.e., does the issue have multi-phase structure?
|
|
153
|
+
|
|
154
|
+
Multi-phase issues have **two or more checkbox-bearing sections** — that is, two or more heading-delimited sections that each contain at least one GFM task item. Test that structure directly; do **not** infer it from the presence of headings. Every templated issue carries `## Problem`, `## Evidence`, `## Affected Files`, `## Acceptance Criteria`, and `## Context`, so a heading count is `> 0` universally and says nothing about phase structure. <!-- Fixed: forge#2840 -->
|
|
153
155
|
|
|
154
|
-
Multi-phase issues have checkbox groups separated by phase headings (lines starting with `##` or `###`) where at least one group has unchecked items. Detect this:
|
|
155
156
|
```bash
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
|
|
157
|
+
# Structural test: count heading-delimited sections that contain checkbox items.
|
|
158
|
+
# Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
|
|
159
|
+
# single-phase regardless of how many prose headings surround it.
|
|
160
|
+
#
|
|
161
|
+
# - Fenced code blocks are stripped first: issue bodies routinely embed fenced
|
|
162
|
+
# blocks whose lines start with '#' or contain a literal '- [ ]'. An
|
|
163
|
+
# unterminated fence keeps the original body so later work is never hidden.
|
|
164
|
+
# - ATX headings and setext underlines both delimit sections. The ATX pattern
|
|
165
|
+
# avoids awk interval-quantifier variance across awk implementations.
|
|
166
|
+
# - grep -E / awk only — no PCRE. '^#+ ' needs none.
|
|
167
|
+
FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
|
|
168
|
+
if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
|
|
169
|
+
BODY_STRIPPED="$BODY"
|
|
170
|
+
else
|
|
171
|
+
BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
|
|
172
|
+
fi
|
|
160
173
|
|
|
161
|
-
|
|
174
|
+
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
175
|
+
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
176
|
+
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
177
|
+
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
|
|
178
|
+
END { if (in_section && has) n++; print n+0 }
|
|
179
|
+
')
|
|
180
|
+
|
|
181
|
+
# Sub-issue-tracker guard: a decompose parent whose only checkbox group is
|
|
182
|
+
# '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
|
|
183
|
+
# sub-issues done and close the tracker, so any unchecked GFM task item for an
|
|
184
|
+
# issue forces multi-phase.
|
|
185
|
+
SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
|
|
186
|
+
|
|
187
|
+
# Keep this consuming guard synchronized with Phase 6A's Sync invariant in
|
|
188
|
+
# commands/work-on.md.
|
|
189
|
+
# Both counters are default-expanded: a failed extraction yields an empty string,
|
|
190
|
+
# not 0, which would make the integer test error out rather than evaluate false.
|
|
191
|
+
if [ "${CHECKBOX_SECTIONS:-0}" -ge 2 ] || [ "${SUBISSUE_ITEMS:-0}" -gt 0 ]; then
|
|
162
192
|
# Multi-phase issue: do NOT check off any [ ] items
|
|
163
193
|
# Only add the PR reference so progress is recorded
|
|
164
194
|
UPDATED_BODY="${BODY}"$'\n\n'"**PR**: #{PR_NUMBER} → merged to \`{PR_BASE}\` (phase complete — remaining phases open)"
|
|
165
195
|
gh issue edit {NUMBER} {GH_FLAG} --body "{UPDATED_BODY}"
|
|
166
196
|
REMAINING_AFTER="$REMAINING_BEFORE"
|
|
167
197
|
else
|
|
168
|
-
# Single-phase issue: check off all remaining
|
|
169
|
-
UPDATED_BODY=$(
|
|
198
|
+
# Single-phase issue: check off all remaining GFM task items
|
|
199
|
+
UPDATED_BODY=$(printf '%s\n' "$BODY" | sed 's/^\([-*+]\) \[ \]/\1 [x]/g')
|
|
170
200
|
UPDATED_BODY="${UPDATED_BODY}"$'\n\n'"**PR**: #{PR_NUMBER} → merged to \`{PR_BASE}\`"
|
|
171
201
|
gh issue edit {NUMBER} {GH_FLAG} --body "{UPDATED_BODY}"
|
|
172
202
|
REMAINING_AFTER=0
|
|
@@ -97,7 +97,7 @@ From the Decomposition Assessment in the investigation report, extract:
|
|
|
97
97
|
3. Brief description for each sub-issue body
|
|
98
98
|
|
|
99
99
|
For each sub-issue, prepare:
|
|
100
|
-
- **Title**: from investigation report's proposed sub-issue title
|
|
100
|
+
- **Title**: from investigation report's proposed sub-issue title, including the final `{fix|feat|refactor}: ` prefix used in Phase D3
|
|
101
101
|
- **Body**: brief description of scope + `**Parent**: #{NUMBER}` + dependency note if applicable
|
|
102
102
|
- **Labels**: inherit priority label (P0/P1/P2) from parent; do NOT copy workflow labels
|
|
103
103
|
- **Milestone**: same milestone title as parent (if parent has one)
|
|
@@ -106,6 +106,38 @@ For each sub-issue, prepare:
|
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
|
109
|
+
## Phase D2.5: Equivalent Set Check
|
|
110
|
+
|
|
111
|
+
Before creating any sub-issue, check whether the complete planned title set already exists for this parent. This covers an interrupted earlier decomposition: its child issues may exist even though it never reached the `FORGE:DECOMPOSED` comment checked in D1.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# SUB_TITLES contains the final, ordered titles prepared in D2.
|
|
115
|
+
EXPECTED_TITLES=$(printf '%s\n' "${SUB_TITLES[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | sort')
|
|
116
|
+
PARENT_SUB_ISSUES=$(gh issue list {GH_FLAG} --state all \
|
|
117
|
+
--search "\"**Parent**: #{NUMBER}\" in:body" --limit 100 --json number,title,body)
|
|
118
|
+
|
|
119
|
+
# Keep the earliest child for each title as canonical. This also collapses a
|
|
120
|
+
# previously duplicated set without creating a third copy.
|
|
121
|
+
CANONICAL_SUB_ISSUES=$(jq --arg parent "**Parent**: #{NUMBER}" --argjson expected "$EXPECTED_TITLES" '
|
|
122
|
+
([.[] | select(.body | contains($parent)) | {number, title}]
|
|
123
|
+
| sort_by(.number) | group_by(.title) | map(.[0])) as $canonical
|
|
124
|
+
| ($canonical | map(.title) | sort) as $actual
|
|
125
|
+
| if $actual == $expected then $canonical else empty end
|
|
126
|
+
' <<< "$PARENT_SUB_ISSUES")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If `CANONICAL_SUB_ISSUES` is non-empty, the decomposition has already created an equivalent set. Do **not** create issues, edit the parent body, or post another decomposition comment. Preserve those original sub-issues and EXIT with:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
DECOMPOSE_RESULT:
|
|
133
|
+
status: ALREADY_DONE
|
|
134
|
+
sub_issues: {CANONICAL_SUB_ISSUES}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If no exact title-set match exists, continue. A partial set is not equivalent: let `/issue` dedup protect its existing children while creating only the missing work.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
109
141
|
## Phase D3: Create Sub-Issues
|
|
110
142
|
|
|
111
143
|
For each sub-issue (in dependency order), route creation through the `/issue` create-hook's programmatic invocation contract (`commands/issue.md` Programmatic Invocation Contract, added in #2085) instead of calling `gh issue create` directly. `/issue`'s Phase 2D runs the same `scripts/issue-dedup.sh` check this file used to run manually — the standalone pre-check below is removed; dedup is now enforced inside the create-hook on every call, with no bypass path. <!-- Changed: forge#2086 — route through /issue create-hook -->
|
|
@@ -481,6 +481,36 @@ After the FORGE:INVESTIGATOR comment is posted, create a structured GitHub Gist
|
|
|
481
481
|
|
|
482
482
|
**This phase is non-blocking** — if Gist creation fails (auth error, rate limit, network), log the failure and continue to Phase 1D. Do NOT stall the pipeline for a knowledge artifact.
|
|
483
483
|
|
|
484
|
+
### Step 0: Check cached Gist capability
|
|
485
|
+
|
|
486
|
+
`FORGE_GIST_CAPABLE` is set once by `/orchestrate` before it dispatches workers. A standalone
|
|
487
|
+
`/work-on` run has no parent cache, so it performs the same read-only identity probe once for its
|
|
488
|
+
own process. GitHub App installation identities are `Bot` users and cannot use the Gists API.
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
if [ -z "${FORGE_GIST_CAPABLE+x}" ]; then
|
|
492
|
+
GIST_AUTH_TYPE=$(gh api user --jq '.type' 2>/dev/null || true)
|
|
493
|
+
if [ "$GIST_AUTH_TYPE" = "Bot" ]; then
|
|
494
|
+
FORGE_GIST_CAPABLE=false
|
|
495
|
+
else
|
|
496
|
+
# Preserve the existing behavior when the identity probe is unavailable or is a PAT user.
|
|
497
|
+
FORGE_GIST_CAPABLE=true
|
|
498
|
+
fi
|
|
499
|
+
export FORGE_GIST_CAPABLE
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
if [ "$FORGE_GIST_CAPABLE" != "true" ]; then
|
|
503
|
+
echo "INFO: Knowledge Gist subsystem unavailable for this authentication — skipping Gist phases"
|
|
504
|
+
GIST_URL=""
|
|
505
|
+
INDEX_URL=""
|
|
506
|
+
# Skip every remaining step in Phases 1C.5 and 1C.6; continue directly to Phase 1D.
|
|
507
|
+
fi
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
When `FORGE_GIST_CAPABLE` is not `true`, do not call `gh gist create`, `gh gist view`,
|
|
511
|
+
`gh gist edit`, or any other Gist command in either Phase 1C.5 or Phase 1C.6. This is an
|
|
512
|
+
informational skip, not a warning, retry, or escalation.
|
|
513
|
+
|
|
484
514
|
### Step 1: Check for existing Gist annotation
|
|
485
515
|
|
|
486
516
|
```bash
|
|
@@ -584,6 +614,9 @@ fi
|
|
|
584
614
|
|
|
585
615
|
**Skip if**: The issue has no milestone (`MILESTONE` is `"none"` or empty).
|
|
586
616
|
|
|
617
|
+
**Also skip if**: `FORGE_GIST_CAPABLE` from Phase 1C.5 is not `true`. Do not independently
|
|
618
|
+
re-probe capability here; the cached result covers both Gist phases.
|
|
619
|
+
|
|
587
620
|
After the per-issue Knowledge Gist is created (Phase 1C.5), update the milestone-level index Gist. The index aggregates all investigation Gist URLs for a milestone into a single reference document. Any agent working on a milestone issue can fetch one index URL to get full context across all investigations.
|
|
588
621
|
|
|
589
622
|
**This phase is non-blocking** — if index creation or update fails, log the warning and continue to Phase 1D. Do NOT stall the pipeline for the index.
|
|
@@ -20,7 +20,7 @@ argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--
|
|
|
20
20
|
|
|
21
21
|
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` PR's own remediation. It does NOT implement the `needs-human` sub-label taxonomy (#1815's scope) and it does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810) — that guard's existing safe-default (`workflow:awaiting-merge` on any clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check *after* that guard has already fired.
|
|
22
22
|
|
|
23
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/remediate`) and completion marker (`FORGE:REMEDIATION:COMPLETE`, including the `**Re-gate outcome**` field Phase M8 posts below) are
|
|
23
|
+
**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.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -110,7 +110,19 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
110
110
|
|
|
111
111
|
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (it should already be present), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
112
112
|
|
|
113
|
-
**If at least one FIXABLE item exists**:
|
|
113
|
+
**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:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
117
|
+
echo "DRY_RUN: would replace needs-human with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
118
|
+
else
|
|
119
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} \
|
|
120
|
+
--add-label "workflow:in-review" \
|
|
121
|
+
--remove-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
122
|
+
fi
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Do not perform this transition for an UNFIXABLE policy escalation. Any later quality-gate, push, or re-review block re-adds `needs-human`; after re-review, remove `workflow:in-review` whenever that terminal label is present.
|
|
114
126
|
|
|
115
127
|
---
|
|
116
128
|
|
|
@@ -217,10 +229,33 @@ Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix y
|
|
|
217
229
|
Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
218
230
|
```
|
|
219
231
|
|
|
220
|
-
|
|
232
|
+
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), run this required re-review through one native foreground `task`:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
if DRY_RUN=true:
|
|
236
|
+
record "Would invoke the foreground re-review task for PR #{PR_NUMBER}."
|
|
237
|
+
else:
|
|
238
|
+
task(
|
|
239
|
+
description="Re-review PR #{PR_NUMBER}",
|
|
240
|
+
subagent_type="general",
|
|
241
|
+
background=false,
|
|
242
|
+
prompt="Load commands/review-pr.md and execute it for PR {PR_NUMBER} with --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}. Return only the structured REVIEW_RESULT block after the review reaches its outcome."
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Wait for the completed child result and retain its `REVIEW_RESULT` in remediation state before continuing to Phase M7. A running/progress response is not a completion result. If the child errors or does not return a parseable `REVIEW_RESULT`, stop with `REMEDIATE_RESULT: status: BLOCKED`; do not report remediation in progress as a terminal parent result. The `Skill(...)` invocation above remains the non-OpenCode path.
|
|
247
|
+
|
|
248
|
+
This re-runs the full review (domain agents → verdict → Phase 8 auto-merge gate). The FIXABLE transition above left the issue at the non-terminal `workflow:in-review` state. `review-pr.md` recognizes the in-progress `FORGE:REMEDIATION` marker posted in Phase M5 as evidence of the prior escalation, so one of two things happens inside Phase 8:
|
|
249
|
+
|
|
250
|
+
- **Re-escalated**: the re-review itself trips a fresh block (`CHANGES REQUESTED`, purpose-regression, calibration, trust, or a still-`CONFLICTING` mergeability check) → it adds `needs-human`, and this phase removes `workflow:in-review`, leaving one terminal state.
|
|
251
|
+
- **Clean re-review**: `VERDICT=APPROVED`-equivalent, mergeable, and the "Previously-escalated re-review guard" (forge#1810) fires — setting `workflow:awaiting-merge` and removing `workflow:in-review`, *without* auto-merging (that guard's own safe default, left untouched by this file).
|
|
221
252
|
|
|
222
|
-
|
|
223
|
-
|
|
253
|
+
```bash
|
|
254
|
+
POST_REVIEW_LABELS=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(",")')
|
|
255
|
+
if echo "$POST_REVIEW_LABELS" | grep -qE '(^|,)needs-human(,|$)'; then
|
|
256
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "workflow:in-review" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
257
|
+
fi
|
|
258
|
+
```
|
|
224
259
|
|
|
225
260
|
Extract the re-review verdict for the paper trail (Phase M8 reports this verbatim):
|
|
226
261
|
```bash
|
|
@@ -203,6 +203,15 @@ If `ATTRIBUTION_PR_FOOTER` is `true`, append the following footer to the PR body
|
|
|
203
203
|
|
|
204
204
|
### R2C: Create PR
|
|
205
205
|
|
|
206
|
+
For a batch issue, construct a non-closing reference line from `BATCH_MEMBERS` before creating the PR:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
BATCH_MEMBER_REFS=""
|
|
210
|
+
if [ "${IS_BATCH:-0}" = "1" ]; then
|
|
211
|
+
BATCH_MEMBER_REFS=$(printf 'Refs #%s\n' "${BATCH_MEMBERS[@]}")
|
|
212
|
+
fi
|
|
213
|
+
```
|
|
214
|
+
|
|
206
215
|
```bash
|
|
207
216
|
gh pr create {GH_FLAG} \
|
|
208
217
|
--base {PR_BASE} \
|
|
@@ -223,6 +232,8 @@ gh pr create {GH_FLAG} \
|
|
|
223
232
|
---
|
|
224
233
|
|
|
225
234
|
Closes #{NUMBER}
|
|
235
|
+
${BATCH_MEMBER_REFS}
|
|
236
|
+
**Batch member disposition**: The batch issue is the code unit being closed. Referenced members that require human or operator action remain open as a split outcome.
|
|
226
237
|
|
|
227
238
|
**Implementation branch**: \`{BRANCH}\`
|
|
228
239
|
**Base**: \`{PR_BASE}\`
|
|
@@ -278,6 +289,22 @@ Invoke the review command:
|
|
|
278
289
|
Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
279
290
|
```
|
|
280
291
|
|
|
292
|
+
**OpenCode joined-child contract**: When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), invoke this load-bearing review through one native foreground `task` instead of treating the `Skill(...)` line as an asynchronous handoff:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
if DRY_RUN=true:
|
|
296
|
+
record "Would invoke the foreground review task for PR #{PR_NUMBER}."
|
|
297
|
+
else:
|
|
298
|
+
task(
|
|
299
|
+
description="Review PR #{PR_NUMBER}",
|
|
300
|
+
subagent_type="general",
|
|
301
|
+
background=false,
|
|
302
|
+
prompt="Load commands/review-pr.md and execute it for PR {PR_NUMBER} with --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}. Return only the structured REVIEW_RESULT block after the review reaches its outcome."
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Wait for that task's completed result before Phase R4. Propagate its `REVIEW_RESULT` as this module's child state; do not return `REVIEW_RESULT`, report progress, release an orchestrator slot, or begin close work while the child is running. If the child errors or returns no parseable `REVIEW_RESULT`, return `REVIEW_RESULT: status: BLOCKED` with the child failure as the blocker. The normal `Skill(...)` invocation above remains the non-OpenCode path.
|
|
307
|
+
|
|
281
308
|
/review-pr handles: full domain-agent review → post findings as separate issues (non-blocking) → merge the PR → close the issue → clean up worktree.
|
|
282
309
|
|
|
283
310
|
---
|