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.
Files changed (49) hide show
  1. package/README.md +2 -1
  2. package/bin/engine/admission.mjs +242 -7
  3. package/bin/engine/admission.test.mjs +183 -2
  4. package/bin/engine/phases.mjs +31 -22
  5. package/bin/engine/resolve.mjs +8 -1
  6. package/bin/engine/resolve.test.mjs +5 -0
  7. package/bin/engine-cli.mjs +2 -0
  8. package/bin/engine.mjs +26 -17
  9. package/bin/forgedock.mjs +124 -48
  10. package/bin/hooks/pre-tool-use.mjs +573 -2
  11. package/bin/journey.mjs +11 -7
  12. package/bin/labels.json +5 -0
  13. package/bin/opencode-adapter.mjs +652 -92
  14. package/bin/orchestrate-preflight.mjs +434 -0
  15. package/bin/runner.mjs +77 -9
  16. package/commands/audit-agents.md +75 -26
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +10 -5
  19. package/commands/issue.md +95 -35
  20. package/commands/orchestrate/phase-1-resolve.md +70 -32
  21. package/commands/orchestrate/phase-3-dependency.md +167 -22
  22. package/commands/orchestrate/phase-4-execution.md +691 -62
  23. package/commands/orchestrate/phase-5-cleanup.md +5 -11
  24. package/commands/orchestrate/phase-6-report.md +27 -1
  25. package/commands/orchestrate/safety.md +1 -1
  26. package/commands/orchestrate.md +39 -1
  27. package/commands/quality-gate.md +30 -10
  28. package/commands/review-pr-agents/protocols.md +13 -0
  29. package/commands/review-pr-agents.md +8 -0
  30. package/commands/review-pr-staging.md +117 -17
  31. package/commands/review-pr.md +95 -23
  32. package/commands/work-on/build/validate.md +25 -4
  33. package/commands/work-on/close.md +40 -10
  34. package/commands/work-on/decompose.md +33 -1
  35. package/commands/work-on/investigate.md +33 -0
  36. package/commands/work-on/remediate.md +40 -5
  37. package/commands/work-on/review.md +27 -0
  38. package/commands/work-on.md +48 -5
  39. package/docs/CONFIG.md +51 -15
  40. package/docs/OPENCODE.md +128 -25
  41. package/docs/spec/review-protocol.md +7 -0
  42. package/package.json +1 -1
  43. package/scripts/check-admission-parity.mjs +1 -0
  44. package/scripts/check-command-side-effects.sh +14 -13
  45. package/scripts/check-shell-portability.sh +36 -0
  46. package/scripts/check-spec-markers.sh +21 -11
  47. package/scripts/checkbox-sections.test.sh +67 -0
  48. package/scripts/extract-affected-files.sh +85 -5
  49. package/scripts/extract-affected-files.test.sh +154 -2
@@ -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
- **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. Each agent posts findings directly to the PR via `gh pr comment`.
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
- FINDING_ISSUE_BODY_FILE=$(mktemp)
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
- When this slug appears on 3+ findings, a check-promotion issue is automatically filed.
1759
- Keep the slug consistent across all findings for the same defect class. --> <!-- Added: forge#1331 -->
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 has no machine-readable return contract (it's a user-facing command, not a work-on
1833
- # subcommand) — resolve the created issue's number by exact-title search immediately after
1834
- # the call. The title embeds ${PR_NUMBER} and the finding summary, making it unique enough
1835
- # for a reliable single-match lookup. Retry to absorb GitHub Search API indexing lag.
1836
- ISSUE_NUM=""
1837
- for _resolve_attempt in 1 2 3; do
1838
- ISSUE_NUM=$(gh issue list -R ${REPO} --search "in:title \"${FINDING_ISSUE_TITLE}\"" --state open --limit 1 --json number --jq '.[0].number // empty')
1839
- [ -n "$ISSUE_NUM" ] && break
1840
- sleep 2
1841
- done
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, this PR was escalated at some
2135
- # earlier point (VERDICT/purpose-regression/calibration/trust/mergeability) and has
2136
- # now been remediated + re-reviewed back to a clean, mergeable APPROVED state above.
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] | any(. == "needs-human")' 2>/dev/null || echo "false")
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. If the agent's own recollection of what it launched disagrees with `ACTUAL_AGENT_COUNT` (e.g. it believes it ran agents but zero `FORGE:REVIEW-AGENT` comments exist), that mismatch itself is the signal this check exists to surface: report `ACTUAL_AGENT_COUNT` as-is (it is the ground truth) and add a note in the Recommendation section flagging the discrepancy — never suppress it to make the summary look complete. If `ACTUAL_AGENT_COUNT` is `0`, the review degraded to solo/inline mode (the exact failure mode this guard exists to catch) and the verdict MUST reflect that (`NEEDS RE-REVIEW`), regardless of what analysis was performed inline.
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
- eval "$PYTHON_FORMAT" 2>&1
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
- eval "$TS_FORMAT" 2>&1
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
- eval "$TS_TYPECHECK" 2>&1
196
+ run_verification_command "typescript.typecheck" "$TS_TYPECHECK"
177
197
  TS_EXIT=$?
178
198
  elif [ -n "$TS_BUILD" ]; then
179
- eval "$TS_BUILD" 2>&1 | tail -30
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=$(echo "$BODY" | grep -c '^- \[ \]' || true)
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 `- [ ]` items will still remain after a full check-off — i.e., does the issue have multi-phase structure?
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
- # Conservative heuristic: if the issue body contains a phase heading (## or ###)
157
- # AND unchecked items remain, treat as multi-phase — do NOT check off items from
158
- # future phases; only add the PR reference and leave unchecked items intact.
159
- HAS_PHASE_HEADINGS=$(echo "$BODY" | grep -cP '^#{2,3} ' || true)
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
- if [ "$HAS_PHASE_HEADINGS" -gt 0 ]; then
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 [ ] items
169
- UPDATED_BODY=$(echo "$BODY" | sed 's/^- \[ \]/- [x]/g')
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 now registered in the headless engine's phase table — `RESERVED_TYPES.REMEDIATION` in `packages/protocol/src/types.js`, `remediate` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and a matching `remediate` entry in `bin/engine/phases.mjs`'s `PHASES` array (`entryCondition`: review committed + `terminalReason: "needs-human"`; `detectOutcome` maps each re-gate outcome to the right terminal reason). **Known limitation, documented in that engine entry's own comment**: a single continuous `runIssue()` walk cannot reach this phase today — `review`'s `"blocked"` outcome and the needs-human divergence guard both terminate the run before `remediate`'s `entryCondition` is ever evaluated. This subcommand therefore continues to run exactly as documented above — as its own separate top-level entry point — until a follow-up issue live-wires the engine to reconstruct state from GitHub for a dedicated remediation run.
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**: proceed to Phase M2.
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
- This re-runs the full review (domain agents → verdict → Phase 8 auto-merge gate). Because `{ISSUE_NUMBER}` still carries `needs-human` at this point, one of two things happens inside `review-pr.md`'s existing, **unedited** Phase 8:
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
- - **Re-escalated**: the re-review itself trips a fresh block (`CHANGES REQUESTED`, purpose-regression, calibration, trust, or a still-`CONFLICTING` mergeability check) → `needs-human` remains set, no merge attempted.
223
- - **Clean re-review**: `VERDICT=APPROVED`-equivalent, mergeable, and the "Previously-escalated re-review guard" (forge#1810) fires — clearing `needs-human` and setting `workflow:awaiting-merge`, *without* auto-merging (that guard's own safe default, left untouched by this file).
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
  ---