forgedock 1.1.7 → 1.1.9
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 +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
|
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG]
|
|
|
12
12
|
**Invoked by**: `work-on.md` routing loop, when `INVESTIGATE_RESULT.decompose = YES`.
|
|
13
13
|
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set labels. STOP — each sub-issue runs its own /work-on.
|
|
14
14
|
|
|
15
|
-
**Agent model policy**: `model: "
|
|
15
|
+
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
|
|
16
16
|
**NEVER use plan mode (EnterPlanMode).**
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -11,7 +11,7 @@ argument-hint: [issue number] [--repo {owner}/{repo}] [--gh-flag "-R {owner}/{re
|
|
|
11
11
|
|
|
12
12
|
Standalone investigation phase for the work-on pipeline. Validates whether an issue is real, determines root cause, posts a structured FORGE:INVESTIGATOR comment to GitHub, and updates workflow labels.
|
|
13
13
|
|
|
14
|
-
**Agent model policy**: `model: "
|
|
14
|
+
**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 (1A label set, `FORGE:CHECKPOINT` writes) stay at this tier because they're interleaved with the reasoning-heavy investigation steps in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
|
|
15
15
|
**NEVER use plan mode (EnterPlanMode).**
|
|
16
16
|
|
|
17
17
|
<!-- FORGE:SPEC_LOADED — work-on/investigate.md loaded and active. Agent is bound by this spec. -->
|
|
@@ -90,6 +90,171 @@ Print these blocks to stdout before Phase 1A begins. During Phase 1B (step 3 —
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
+
## Phase 0.6: Forge Ledger Pre-Recall <!-- Added: forge#1740 -->
|
|
94
|
+
|
|
95
|
+
**Goal**: Query the Forge Ledger knowledge index for prior cards matching this issue's title terms, affected files, and symbols — **before reading any code**. Inject above-threshold results with explicit delta-verification framing so the investigator builds on prior knowledge rather than re-deriving it.
|
|
96
|
+
|
|
97
|
+
**This phase is non-blocking** — if the index is absent, empty, or returns no match above threshold, log a single line and proceed to Phase 1A. Never stall the pipeline for recall.
|
|
98
|
+
|
|
99
|
+
**Relationship to Phase 0.5**: Phase 0.5 retrieves structured priors from the Gist-based FORGE:MEMORY_INDEX (coarse keyword scoring over MEMORY_ENTRY lines). Phase 0.6 queries the Forge Ledger index (BM25 over structured knowledge cards, indexed by `build-knowledge-index.mjs`). Both are complementary and both run — neither replaces the other.
|
|
100
|
+
|
|
101
|
+
### Step 1: Probe for index
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
RECALL_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/bin/recall.mjs"
|
|
105
|
+
LEDGER_AVAILABLE=0
|
|
106
|
+
|
|
107
|
+
if [ -f "$RECALL_PATH" ]; then
|
|
108
|
+
# Quick probe: does the index exist and have at least one card?
|
|
109
|
+
PROBE=$(node "$RECALL_PATH" --doctor 2>/dev/null | grep "^Total cards:" | grep -v "^Total cards: 0" || true)
|
|
110
|
+
[ -n "$PROBE" ] && LEDGER_AVAILABLE=1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ "$LEDGER_AVAILABLE" -eq 0 ]; then
|
|
114
|
+
echo "[recall] Forge Ledger index absent or empty — skipping Phase 0.6, proceeding to Phase 1A"
|
|
115
|
+
# → Continue to Phase 1A
|
|
116
|
+
fi
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Step 2: Build combined query (title terms + symbols + affected files)
|
|
120
|
+
|
|
121
|
+
Extract query components from the issue body and title. The recall CLI accepts free text (BM25 ranked) plus repeated `--file` flags (exact-match boost):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Extract title keywords: lowercase, remove stop words, keep noun/verb tokens (≥4 chars)
|
|
125
|
+
ISSUE_TITLE=$(gh issue view {NUMBER} {GH_FLAG} --json title --jq '.title' 2>/dev/null || echo '')
|
|
126
|
+
TITLE_TERMS=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' \
|
|
127
|
+
| sed 's/[^a-z0-9 ]/ /g' \
|
|
128
|
+
| tr ' ' '\n' \
|
|
129
|
+
| grep -E '^[a-z]{4,}$' \
|
|
130
|
+
| grep -vE '^(with|that|this|from|into|have|will|when|then|than|them|they|been|were|also|only|does|some|each|more|over|such|both|most|other|many|after|about|should|would|could|their|these|those|which|where|there|being|while|using|since|until|before|under|above)$' \
|
|
131
|
+
| sort -u | head -10 | tr '\n' ' ' | xargs)
|
|
132
|
+
|
|
133
|
+
# Extract affected files from the issue body's ## Affected Files section
|
|
134
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' 2>/dev/null || echo '')
|
|
135
|
+
AFFECTED_FILES_RAW=$(echo "$ISSUE_BODY" \
|
|
136
|
+
| sed -n '/^## Affected Files/,/^## /p' \
|
|
137
|
+
| grep -oE '`[^`]+\.(md|mjs|js|ts|py|sh|yaml|yml|json)`' \
|
|
138
|
+
| tr -d '`' | sort -u | head -5)
|
|
139
|
+
|
|
140
|
+
# Extract symbols: backtick-quoted identifiers from the issue body (≥4 chars, camelCase or snake_case)
|
|
141
|
+
SYMBOLS_RAW=$(echo "$ISSUE_BODY" \
|
|
142
|
+
| grep -oE '`[A-Za-z_][A-Za-z0-9_]{3,}`' \
|
|
143
|
+
| tr -d '`' | sort -u | head -5)
|
|
144
|
+
|
|
145
|
+
# Build --file flags (one per affected file)
|
|
146
|
+
FILE_FLAGS=""
|
|
147
|
+
while IFS= read -r f; do
|
|
148
|
+
[ -n "$f" ] && FILE_FLAGS="$FILE_FLAGS --file $f"
|
|
149
|
+
done <<< "$AFFECTED_FILES_RAW"
|
|
150
|
+
|
|
151
|
+
# Build --symbol flag (first symbol only — recall supports one --symbol)
|
|
152
|
+
SYMBOL_FLAG=""
|
|
153
|
+
FIRST_SYMBOL=$(echo "$SYMBOLS_RAW" | head -1)
|
|
154
|
+
[ -n "$FIRST_SYMBOL" ] && SYMBOL_FLAG="--symbol $FIRST_SYMBOL"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Step 3: Run recall query and capture results
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
RECALL_RESULTS=""
|
|
161
|
+
RECALL_ISSUE_CITATIONS=""
|
|
162
|
+
|
|
163
|
+
if [ "$LEDGER_AVAILABLE" -eq 1 ] && [ -n "$TITLE_TERMS$FILE_FLAGS$SYMBOL_FLAG" ]; then
|
|
164
|
+
echo "[recall] Querying Forge Ledger: terms='${TITLE_TERMS}' files='${AFFECTED_FILES_RAW}' symbol='${FIRST_SYMBOL}'"
|
|
165
|
+
|
|
166
|
+
# Run combined query: title terms (free text) + file flags + symbol + min-score threshold
|
|
167
|
+
# --json for machine-readable output; --min-score 0.3 filters weak matches (noise gate)
|
|
168
|
+
RECALL_JSON=$(node "$RECALL_PATH" \
|
|
169
|
+
$TITLE_TERMS \
|
|
170
|
+
$FILE_FLAGS \
|
|
171
|
+
$SYMBOL_FLAG \
|
|
172
|
+
--k 5 \
|
|
173
|
+
--min-score 0.3 \
|
|
174
|
+
--json \
|
|
175
|
+
2>/dev/null || echo '[]')
|
|
176
|
+
|
|
177
|
+
# Count results
|
|
178
|
+
RECALL_COUNT=$(echo "$RECALL_JSON" | node -e "
|
|
179
|
+
let d = ''; process.stdin.on('data', c => d += c);
|
|
180
|
+
process.stdin.on('end', () => {
|
|
181
|
+
try { console.log(JSON.parse(d).length); } catch { console.log(0); }
|
|
182
|
+
});
|
|
183
|
+
" 2>/dev/null || echo "0")
|
|
184
|
+
|
|
185
|
+
if [ "${RECALL_COUNT:-0}" -gt 0 ]; then
|
|
186
|
+
echo "[recall] Found ${RECALL_COUNT} prior card(s) above threshold — injecting with delta-verification framing"
|
|
187
|
+
|
|
188
|
+
# Extract issue citations for the Prior Investigations field in the FORGE:INVESTIGATOR comment
|
|
189
|
+
RECALL_ISSUE_CITATIONS=$(echo "$RECALL_JSON" | node -e "
|
|
190
|
+
let d = ''; process.stdin.on('data', c => d += c);
|
|
191
|
+
process.stdin.on('end', () => {
|
|
192
|
+
try {
|
|
193
|
+
const cards = JSON.parse(d);
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const cites = cards
|
|
196
|
+
.filter(c => { if (seen.has(c.issue)) return false; seen.add(c.issue); return true; })
|
|
197
|
+
.map(c => '#' + c.issue)
|
|
198
|
+
.join(', ');
|
|
199
|
+
console.log(cites);
|
|
200
|
+
} catch { console.log(''); }
|
|
201
|
+
});
|
|
202
|
+
" 2>/dev/null || echo '')
|
|
203
|
+
|
|
204
|
+
# Format cards as human-readable text for injection (not raw JSON)
|
|
205
|
+
RECALL_FORMATTED=$(echo "$RECALL_JSON" | node -e "
|
|
206
|
+
let d = ''; process.stdin.on('data', c => d += c);
|
|
207
|
+
process.stdin.on('end', () => {
|
|
208
|
+
try {
|
|
209
|
+
const cards = JSON.parse(d);
|
|
210
|
+
const lines = [];
|
|
211
|
+
for (const c of cards) {
|
|
212
|
+
lines.push('── ' + c.kind.toUpperCase() + ' from #' + c.issue + ' (score: ' + c.score.toFixed(2) + ') ──');
|
|
213
|
+
if (c.rootCause) lines.push('Root Cause: ' + c.rootCause);
|
|
214
|
+
if (c.prevention) lines.push('Fix/Prevention: ' + c.prevention);
|
|
215
|
+
if (c.pattern) lines.push('Pattern: ' + c.pattern);
|
|
216
|
+
if (c.verdict) lines.push('Verdict: ' + c.verdict + ' (' + (c.confidence || '?') + ')');
|
|
217
|
+
if (c.paths && c.paths.length) lines.push('Files: ' + c.paths.slice(0,3).join(', '));
|
|
218
|
+
}
|
|
219
|
+
console.log(lines.join('\n'));
|
|
220
|
+
} catch (e) { console.log(''); }
|
|
221
|
+
});
|
|
222
|
+
" 2>/dev/null || echo '')
|
|
223
|
+
|
|
224
|
+
# Cap at ≤ 2K chars (same budget as context C0 Gist summaries)
|
|
225
|
+
RECALL_RESULTS=$(echo "$RECALL_FORMATTED" | head -c 2048)
|
|
226
|
+
else
|
|
227
|
+
echo "[recall] No prior cards above threshold (min-score 0.3) — investigating from scratch"
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Step 4: Inject recall results into investigation context
|
|
233
|
+
|
|
234
|
+
If `RECALL_RESULTS` is non-empty, print the following block to stdout **before Phase 1A begins**. The investigation steps in Phase 1B MUST treat this as starting context — verify deltas against current code rather than re-deriving the prior model.
|
|
235
|
+
|
|
236
|
+
**If `RECALL_RESULTS` is empty**: skip this step entirely and proceed to Phase 1A.
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
[FORGE:RECALL_PRIOR — Forge Ledger pre-recall results]
|
|
240
|
+
Prior cards matched this issue above threshold (min-score 0.3).
|
|
241
|
+
|
|
242
|
+
CRITICAL FRAMING: These cards describe prior findings on the same files/symbols.
|
|
243
|
+
→ Verify the deltas against current code — do NOT re-derive the prior model.
|
|
244
|
+
→ Stale cards (status: stale) are excluded by default — the query runs without
|
|
245
|
+
--include-stale. Do not treat any card as authoritative without confirming
|
|
246
|
+
the finding still exists in the current code.
|
|
247
|
+
→ Cite confirmed priors in the FORGE:INVESTIGATOR "Prior Investigations" field.
|
|
248
|
+
|
|
249
|
+
${RECALL_RESULTS}
|
|
250
|
+
|
|
251
|
+
[END FORGE:RECALL_PRIOR]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Store `RECALL_ISSUE_CITATIONS` in a variable — it is used in Phase 1C to populate the `**Prior Investigations (via recall)**` field in the FORGE:INVESTIGATOR comment.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
93
258
|
## Phase 1A: Load Issue & Check Resume State
|
|
94
259
|
|
|
95
260
|
```bash
|
|
@@ -192,6 +357,20 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
192
357
|
|
|
193
358
|
The comment MUST include `<!-- INVESTIGATION:COMPLETE -->` at the very end, AFTER all required sections are present. This marker signals the investigation finished successfully.
|
|
194
359
|
|
|
360
|
+
**CODEC PATH (forge#1727)**: Construct the annotation body via the protocol codec — do NOT hand-roll the `<!-- FORGE:INVESTIGATOR -->` header. Use `forge-annotation.sh write INVESTIGATOR --field ...` or `node packages/protocol/src/cli.js emit INVESTIGATOR --field ...` to produce the opening tag and completion sentinel. Fill in the Markdown body sections below. The full pattern:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Build the annotation body via codec (escaping and sentinel handled by codec)
|
|
364
|
+
ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
365
|
+
--field "Verdict={VERDICT}" \
|
|
366
|
+
--field "Confidence={CONFIDENCE}" \
|
|
367
|
+
--field "Severity={SEVERITY}" \
|
|
368
|
+
--field "Task Type={TASK_TYPE}" \
|
|
369
|
+
--field "Decomposition Assessment={YES|NO} — {reason}")
|
|
370
|
+
# ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
|
|
371
|
+
# Append the Markdown body sections to it before posting.
|
|
372
|
+
```
|
|
373
|
+
|
|
195
374
|
Before posting, resolve the attribution annotation link from `forge.yaml`:
|
|
196
375
|
|
|
197
376
|
```bash
|
|
@@ -234,6 +413,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
234
413
|
**Last touched**: {hash — author — date — subject}
|
|
235
414
|
**Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
|
|
236
415
|
{This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
|
|
416
|
+
**Prior Investigations (via recall)**: {Comma-separated issue citations from \`RECALL_ISSUE_CITATIONS\` (e.g. '#1172, #1243 — building on, not repeating'), or 'None — no Forge Ledger match above threshold' if \`RECALL_RESULTS\` was empty. Use the \`RECALL_ISSUE_CITATIONS\` variable populated in Phase 0.6.}
|
|
237
417
|
|
|
238
418
|
### Recommendation
|
|
239
419
|
{what to build/fix, concrete and actionable}
|
|
@@ -245,12 +425,19 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
245
425
|
**{YES|NO}** — {reason}
|
|
246
426
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
247
427
|
|
|
248
|
-
### Acceptance Spec
|
|
428
|
+
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
249
429
|
{For each item in the issue's ## Acceptance Criteria section, emit one machine-checkable check line using the format below. If the issue has no Acceptance Criteria section, derive checks from the Recommendation above. Each check MUST be specific, observable, and testable — not vague prose. Checks are consumed by build/validate Phase B6.5 as the merge gate.}
|
|
250
430
|
|
|
431
|
+
**Quoting (MANDATORY)**: `target=` and `matcher=` MUST always be wrapped in double quotes — `target="..."` / `matcher="..."` — even when the value is a single token (e.g. a plain file path). The downstream Phase B6.5 parser only extracts quoted values; an unquoted `target=`/`matcher=` will silently truncate at the first space and cause a false-negative gate failure for any multi-word value (shell commands with flags/arguments/pipes are almost always multi-word). Neither `target` nor `matcher` may contain a literal `"` character — use single quotes for any embedded string/regex literal inside the value, as shown below. `id=` and `type=` are always single tokens and are never quoted. `description=` is always the last field on the line and is captured to end-of-line — it does not need quoting.
|
|
432
|
+
|
|
251
433
|
```
|
|
252
|
-
ACCEPTANCE_CHECK: id={ac-1} type={exists|contains|command|behavior} target={file_path|command|url} matcher={string|exit_0|regex} description={one-line human description}
|
|
253
|
-
ACCEPTANCE_CHECK: id={ac-2} type={exists|contains|command|behavior} target={file_path|command|url} matcher={string|exit_0|regex} description={one-line human description}
|
|
434
|
+
ACCEPTANCE_CHECK: id={ac-1} type={exists|contains|command|behavior} target="{file_path|command|url}" matcher="{string|exit_0|regex}" description={one-line human description}
|
|
435
|
+
ACCEPTANCE_CHECK: id={ac-2} type={exists|contains|command|behavior} target="{file_path|command|url}" matcher="{string|exit_0|regex}" description={one-line human description}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Example with a multi-word shell command target (the case that previously broke — note the embedded single quotes around the regex, and the double quotes wrapping the whole target):
|
|
439
|
+
```
|
|
440
|
+
ACCEPTANCE_CHECK: id=ac-4 type=command target="grep -qE '(>= ?2|2\+)' commands/orchestrate/phase-1-resolve.md" matcher="exit_0" description=Fan-out cap is documented as >=2 or 2+
|
|
254
441
|
```
|
|
255
442
|
|
|
256
443
|
**Check types**:
|
|
@@ -259,7 +446,9 @@ ACCEPTANCE_CHECK: id={ac-2} type={exists|contains|command|behavior} target={file
|
|
|
259
446
|
- `command` — run a shell command and assert exit 0 (`target` = shell command, `matcher` = `exit_0`)
|
|
260
447
|
- `behavior` — assert a runtime/observable behavior via shell command (`target` = shell command, `matcher` = expected output string or regex)
|
|
261
448
|
|
|
262
|
-
**
|
|
449
|
+
**Self-defeating pipe guideline**: do NOT chain a `-q`/`--quiet` command into a downstream pipe consumer (e.g. `grep -q ... | grep ...`). A `-q` flag suppresses all stdout, so the next command in the pipe always receives empty input and the check can never pass regardless of the actual file content. If a check needs to verify two conditions against the same output, sequence them instead — e.g. `grep -qE 'first' file && grep -qE 'second' file` — or capture the output once and grep the captured variable.
|
|
450
|
+
|
|
451
|
+
**Skipping**: if the issue has no verifiable acceptance criteria and none can be derived from the recommendation, emit a single sentinel: `ACCEPTANCE_CHECK: id=ac-skip type=skipped target="none" matcher="none" description=No machine-checkable criteria available — human review required`
|
|
263
452
|
${ANNOTATION_LINK_FOOTER}
|
|
264
453
|
<!-- INVESTIGATION:COMPLETE -->"
|
|
265
454
|
```
|
|
@@ -541,6 +730,47 @@ fi
|
|
|
541
730
|
|
|
542
731
|
## Phase 1D: Update Labels & Return Verdict
|
|
543
732
|
|
|
733
|
+
### 1D.0: Finding Lifecycle Label Transition (MANDATORY — run before workflow label update)
|
|
734
|
+
|
|
735
|
+
**Purpose**: Wire the investigation verdict into the finding-validation lifecycle. If the issue under investigation is a review-finding (carries `needs-validation`), translate the verdict into `validated` or `false-positive` using `transition-label.sh --validate`. This is the primary mechanism that resolves the `needs-validation → validated/false-positive` lifecycle gap. <!-- Added: forge#1730 -->
|
|
736
|
+
|
|
737
|
+
**Run this block regardless of verdict (CONFIRMED, PARTIAL, INVALID) — the verdict-to-label mapping handles all cases:**
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
# Check if this issue is a finding awaiting validation
|
|
741
|
+
ISSUE_LABELS=$(gh issue view {NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
|
|
742
|
+
|
|
743
|
+
if echo "$ISSUE_LABELS" | grep -q "needs-validation"; then
|
|
744
|
+
echo "Issue #{NUMBER} has needs-validation — applying verdict label transition..."
|
|
745
|
+
RESOLUTION=$(resolve_script 'transition-label')
|
|
746
|
+
TIER="${RESOLUTION%%:*}"
|
|
747
|
+
SCRIPT_PATH="${RESOLUTION#*:}"
|
|
748
|
+
case "$TIER" in
|
|
749
|
+
adaptive|universal)
|
|
750
|
+
bash "$SCRIPT_PATH" --validate {VERDICT} {NUMBER} {GH_FLAG} || true
|
|
751
|
+
;;
|
|
752
|
+
prose)
|
|
753
|
+
# Prose fallback: apply label transition directly via gh
|
|
754
|
+
if [ "{VERDICT}" = "CONFIRMED" ]; then
|
|
755
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "validated" --remove-label "needs-validation" 2>/dev/null || true
|
|
756
|
+
echo "Applied: needs-validation → validated (verdict: CONFIRMED)"
|
|
757
|
+
else
|
|
758
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "false-positive" --remove-label "needs-validation" 2>/dev/null || true
|
|
759
|
+
echo "Applied: needs-validation → false-positive (verdict: {VERDICT})"
|
|
760
|
+
fi
|
|
761
|
+
;;
|
|
762
|
+
esac
|
|
763
|
+
else
|
|
764
|
+
echo "Issue #{NUMBER} does not have needs-validation — no finding lifecycle transition needed"
|
|
765
|
+
fi
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Verdict → label mapping**: `CONFIRMED` → `validated`; `PARTIAL`, `NOT-CONFIRMED`, `INVALID` → `false-positive`.
|
|
769
|
+
|
|
770
|
+
**Idempotency**: `transition-label.sh --validate` is a no-op if the issue already has `validated` or `false-positive`, or if it lacks `needs-validation`. Safe to call on any issue.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
544
774
|
**CONFIRMED or PARTIAL with decompose: NO**:
|
|
545
775
|
```bash
|
|
546
776
|
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating"
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
|
|
3
|
+
argument-hint: [PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]
|
|
4
|
+
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
|
+
|
|
8
|
+
# work-on/remediate — Remediation Subcommand
|
|
9
|
+
|
|
10
|
+
**Input**: $ARGUMENTS
|
|
11
|
+
|
|
12
|
+
**Invoked by**:
|
|
13
|
+
- `work-on.md` Phase 0A, standalone: `/work-on <pr> --remediate` (see forge#1813).
|
|
14
|
+
- `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
|
|
15
|
+
|
|
16
|
+
**Output**: Checkout the PR's existing branch → classify the block reason (fixable vs. policy escalation) → apply fixes → quality-gate → commit/push → re-invoke `/review-pr --auto-merge` → compute the #1809 Q1 auto-land bar → merge-if-verified or hold at `workflow:awaiting-merge` → emit a `FORGE:REMEDIATION` paper trail. Return result to caller.
|
|
17
|
+
|
|
18
|
+
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
|
|
19
|
+
**NEVER use plan mode (EnterPlanMode).**
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Inputs
|
|
26
|
+
|
|
27
|
+
Parse from $ARGUMENTS:
|
|
28
|
+
- `{PR_NUMBER}` — PR number to remediate (required, first positional arg). This is the `needs-human`-gated PR itself, NOT the linked issue number.
|
|
29
|
+
- `--issue {ISSUE_NUMBER}` — linked issue number (optional). If absent, resolved in Phase M0 from the PR body's `Closes #N` reference.
|
|
30
|
+
- `--repo {GH_REPO}` — GitHub repo (resolved from `forge.yaml → project` if omitted)
|
|
31
|
+
- `--gh-flag {GH_FLAG}` — gh CLI repo flag
|
|
32
|
+
- `--base {PR_BASE}` — PR target branch (optional; resolved from the PR's `baseRefName` if omitted)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Phase M0: Load State & Guard Rails (MANDATORY)
|
|
37
|
+
|
|
38
|
+
Re-read current state before doing anything:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
PR_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state,headRefName,baseRefName,body,mergeable,mergeStateStatus,url)
|
|
42
|
+
PR_OPEN_STATE=$(echo "$PR_STATE" | jq -r '.state')
|
|
43
|
+
HEAD_BRANCH=$(echo "$PR_STATE" | jq -r '.headRefName')
|
|
44
|
+
PR_BASE="${PR_BASE:-$(echo "$PR_STATE" | jq -r '.baseRefName')}"
|
|
45
|
+
PR_BODY=$(echo "$PR_STATE" | jq -r '.body')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**PR state guard**:
|
|
49
|
+
- `PR_OPEN_STATE = MERGED` → EXIT `REMEDIATE_RESULT: status: ALREADY_DONE` (nothing to remediate — already landed).
|
|
50
|
+
- `PR_OPEN_STATE = CLOSED` (not merged) → EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "PR #{PR_NUMBER} is closed, not merged — nothing to remediate."
|
|
51
|
+
|
|
52
|
+
**Resolve the linked issue** (`--issue` flag takes precedence; else parse from the PR body — anchored, matching the `"Closes #N" in:body` precedent from forge#1634/#1646, never a bare-number scan):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ISSUE_NUMBER="${ISSUE_NUMBER:-$(echo "$PR_BODY" | grep -oP '(?i)\bCloses #\K\d+' | head -1)}"
|
|
56
|
+
if [ -z "$ISSUE_NUMBER" ]; then
|
|
57
|
+
echo "BLOCKED: cannot resolve linked issue — pass --issue explicitly"
|
|
58
|
+
# EXIT REMEDIATE_RESULT: status: BLOCKED, blocker: "cannot resolve linked issue — pass --issue explicitly"
|
|
59
|
+
fi
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Load the linked issue and validate it is a genuine remediation target**:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
ISSUE_STATE=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels,state,body,milestone)
|
|
66
|
+
ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- If `needs-human` is NOT among `ISSUE_LABELS` → EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "issue #{ISSUE_NUMBER} is not `needs-human` — remediation mode only targets `needs-human`-gated PRs; use the normal `/work-on {ISSUE_NUMBER}` resume path instead." This keeps blast radius scoped to exactly the gap this mode fills — it is not a general-purpose re-review trigger.
|
|
70
|
+
|
|
71
|
+
**Idempotency / resume check** — the paper trail lives on **both** the PR (primary — checked by the orchestrator's item 6.4 dispatch guard) and the linked issue (mirror — keeps `/work-on`'s standard FORGE-annotation trajectory and resume logic consistent with every other phase):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
PR_REMEDIATION_COMMENT=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
|
|
75
|
+
--jq '[.[] | select(.body | contains("FORGE:REMEDIATION"))] | last')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- If a comment is found AND its body contains `FORGE:REMEDIATION:COMPLETE` → EXIT `REMEDIATE_RESULT: status: ALREADY_DONE`. **Single-attempt semantics (AC5)**: once a `FORGE:REMEDIATION:COMPLETE` marker exists for this PR, do NOT re-attempt fixes on a subsequent invocation, regardless of the prior verdict — this is what prevents an infinite remediation retry loop on a genuinely-blocked PR.
|
|
79
|
+
- If a comment is found WITHOUT `:COMPLETE` → a prior attempt was interrupted mid-flight (same failure mode as the investigation phase's partial-comment case). Delete the partial comment(s) on both the PR and the issue, then continue below as a fresh attempt:
|
|
80
|
+
```bash
|
|
81
|
+
gh api repos/{GH_REPO}/issues/comments/{PARTIAL_PR_COMMENT_ID} -X DELETE 2>/dev/null || true
|
|
82
|
+
gh api repos/{GH_REPO}/issues/comments/{PARTIAL_ISSUE_COMMENT_ID} -X DELETE 2>/dev/null || true
|
|
83
|
+
```
|
|
84
|
+
- If no comment is found → fresh attempt, continue below.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Phase M1: Load Prior Findings & Classify the Block Reason
|
|
89
|
+
|
|
90
|
+
Gather everything that caused (or is still causing) `needs-human`:
|
|
91
|
+
|
|
92
|
+
**M1a — Open review-finding issues spawned from this PR** (same title-match precedent as `review-pr.md` Phase 8B/9A):
|
|
93
|
+
```bash
|
|
94
|
+
FINDINGS=$(gh issue list {GH_FLAG} --state open --label "review-finding" --limit 100 \
|
|
95
|
+
--json number,title,body \
|
|
96
|
+
--jq "[.[] | select(.title | test(\"PR #{PR_NUMBER}\"))]")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**M1b — PR review verdicts and merge-block reasons** (Phase 8 of `review-pr.md` records the exact block reason on the linked issue when it aborts auto-merge — read that trail rather than re-deriving it):
|
|
100
|
+
```bash
|
|
101
|
+
BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
102
|
+
--jq '[.[] | select(.body | test("Auto-merge aborted|not mergeable|Pre-Push Ancestry Guard Failed|Push Failed|Quality Gate Failed"; "i"))] | last')
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
106
|
+
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a mergeability guard failure (`CONFLICTING`/`DIRTY`/`BLOCKED` — resolvable by rebasing onto `{PR_BASE}`), or a quality-gate/build failure.
|
|
107
|
+
- **UNFIXABLE (policy escalation)** — `HAS_PURPOSE_REGRESSION=true` (the PR's behavior diverges from the issue's intent — a judgment call, not a code defect), `CALIBRATION_NEEDS_HUMAN=true` (statistical trust threshold), or `TRUST_NEEDS_HUMAN=true` (provenance `NOVEL_NEEDS_HUMAN` tier, insufficient prior data — a policy gate, not a bug). None of these are mechanically "fixable" by re-editing code.
|
|
108
|
+
|
|
109
|
+
**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`."
|
|
110
|
+
|
|
111
|
+
**If at least one FIXABLE item exists**: proceed to Phase M2.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Phase M2: Checkout the PR's Existing Branch
|
|
116
|
+
|
|
117
|
+
Remediation always fixes forward on top of the PR's existing head commit — never rebase onto a different base and never force-push over the PR's history unless a fix genuinely requires it (e.g. resolving a merge conflict per the mergeability guard case, in which case use `git rebase`/`git merge` onto `origin/{PR_BASE}` exactly as the branch's own commit history would, then `--force-with-lease`).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd {REPO_PATH}
|
|
121
|
+
git fetch origin
|
|
122
|
+
WORKTREE_PATH="{WORKTREE_BASE}/remediate-{HEAD_BRANCH_SLUG}-{PR_NUMBER}"
|
|
123
|
+
if [ -d "{WORKTREE_PATH}" ]; then
|
|
124
|
+
git -C "{WORKTREE_PATH}" fetch origin
|
|
125
|
+
git -C "{WORKTREE_PATH}" checkout {HEAD_BRANCH}
|
|
126
|
+
git -C "{WORKTREE_PATH}" reset --hard "origin/{HEAD_BRANCH}"
|
|
127
|
+
else
|
|
128
|
+
git worktree add "{WORKTREE_PATH}" {HEAD_BRANCH} "origin/{HEAD_BRANCH}"
|
|
129
|
+
fi
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If the worktree/branch checkout fails for any reason (branch deleted, force-pushed out from under us, etc.): post a comment, add `needs-human`, EXIT `REMEDIATE_RESULT: status: BLOCKED`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Phase M3: Apply Fixes
|
|
137
|
+
|
|
138
|
+
For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PATH}` before editing (never assume current state), apply the fix. Follow the same implementation discipline as `work-on.md` Phase 3F (cross-lane import guard, library-callback verification, deliverable-type consistency, no unrequested scope) — this file does not restate those rules, it inherits them.
|
|
139
|
+
|
|
140
|
+
**If the block reason was a mergeability conflict** (`CONFLICTING`/`DIRTY`/`BLOCKED`): resolve it by rebasing `{HEAD_BRANCH}` onto `origin/{PR_BASE}` (or merging `{PR_BASE}` in, whichever preserves a clean, reviewable history) — resolve conflicts manually, do not blindly take "ours"/"theirs".
|
|
141
|
+
|
|
142
|
+
**Quality Gate** (same loop as Phase 3G, max 3 iterations):
|
|
143
|
+
```
|
|
144
|
+
iteration = 0
|
|
145
|
+
while iteration < 3:
|
|
146
|
+
iteration += 1
|
|
147
|
+
Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
148
|
+
if result == "QUALITY GATE: PASS": GATE_PASSED=true; break
|
|
149
|
+
else: fix each HIGH/MEDIUM finding, re-stage
|
|
150
|
+
```
|
|
151
|
+
If still failing after 3 iterations: post a comment, re-affirm `needs-human`, EXIT `REMEDIATE_RESULT: status: BLOCKED`. Do not proceed to re-review with an unresolved gate failure — that would just re-escalate one phase later with a worse paper trail.
|
|
152
|
+
|
|
153
|
+
**Format/verify**: run the project's configured `verification.commands` (same as Phase 3H) before committing.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Phase M4: Commit, Push, and Close Addressed Findings
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
cd {WORKTREE_PATH}
|
|
161
|
+
git add -u
|
|
162
|
+
git commit -s -m "fix(remediate): {description} (#{ISSUE_NUMBER})"
|
|
163
|
+
git push origin {HEAD_BRANCH}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If push fails, retry with `--force-with-lease` (expected when M3 rebased to resolve a conflict). If it still fails: post a comment, add `needs-human`, EXIT `REMEDIATE_RESULT: status: BLOCKED`.
|
|
167
|
+
|
|
168
|
+
**Close each addressed review-finding issue directly** (this remediation fixes findings in-place on the existing PR, rather than each finding spawning its own downstream `/work-on` pipeline — leaving them open would have a future run rediscover already-fixed code). Track the closed numbers in `ADDRESSED_FINDING_NUMBERS[]` — Phase M8 reports this array in the final paper trail:
|
|
169
|
+
```bash
|
|
170
|
+
ADDRESSED_FINDING_NUMBERS=()
|
|
171
|
+
for FINDING_NUM in {FIXABLE_FINDING_NUMBERS_FROM_M1}; do
|
|
172
|
+
gh issue close "$FINDING_NUM" {GH_FLAG} \
|
|
173
|
+
--comment "Fixed by remediation of PR #{PR_NUMBER} (commit {COMMIT_SHA}). See #{ISSUE_NUMBER}."
|
|
174
|
+
ADDRESSED_FINDING_NUMBERS+=("$FINDING_NUM")
|
|
175
|
+
done
|
|
176
|
+
```
|
|
177
|
+
Only close findings actually addressed in this commit — leave any FIXABLE-but-deferred or unrelated open findings untouched.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Phase M5: Post Interim FORGE:REMEDIATION Progress (before re-review)
|
|
182
|
+
|
|
183
|
+
Post the same body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` (PR copy is the idempotency source of truth; issue copy keeps the standard trajectory/resume logic consistent):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:REMEDIATION -->
|
|
187
|
+
## Remediation In Progress for PR #{PR_NUMBER}
|
|
188
|
+
|
|
189
|
+
**Findings addressed**:
|
|
190
|
+
{bulleted list: finding # — title — one-line fix summary}
|
|
191
|
+
|
|
192
|
+
**Commit**: {COMMIT_SHA}
|
|
193
|
+
**Quality gate**: {iterations} iteration(s), PASS
|
|
194
|
+
|
|
195
|
+
Re-invoking \`/review-pr --auto-merge\` now."
|
|
196
|
+
gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "<!-- FORGE:REMEDIATION -->
|
|
197
|
+
## Remediation In Progress for PR #{PR_NUMBER}
|
|
198
|
+
|
|
199
|
+
**Findings addressed**:
|
|
200
|
+
{bulleted list: finding # — title — one-line fix summary}
|
|
201
|
+
|
|
202
|
+
**Commit**: {COMMIT_SHA}
|
|
203
|
+
**Quality gate**: {iterations} iteration(s), PASS
|
|
204
|
+
|
|
205
|
+
Re-invoking \`/review-pr --auto-merge\` now."
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix yet — per the marker-presence convention (forge#1360/#1357), the absence of `:COMPLETE` correctly signals "in progress" to any concurrent reader, and the M0 resume check above treats this exact state as an interrupted attempt if a session dies before M8.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Phase M6: Re-Invoke /review-pr
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
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:
|
|
219
|
+
|
|
220
|
+
- **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.
|
|
221
|
+
- **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).
|
|
222
|
+
|
|
223
|
+
Extract the re-review verdict for the paper trail (Phase M8 reports this verbatim):
|
|
224
|
+
```bash
|
|
225
|
+
RE_REVIEW_VERDICT=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
|
|
226
|
+
--jq '[.[] | select(.body | test("APPROVED:|CHANGES REQUESTED:"; "i"))] | last | .body // "unknown"' 2>/dev/null | head -c 200)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Phase M7: Compute the #1809 Q1 Auto-Land Bar
|
|
232
|
+
|
|
233
|
+
Re-read the issue's current labels after M6:
|
|
234
|
+
```bash
|
|
235
|
+
POST_REVIEW_LABELS=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(",")')
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**If `needs-human` is present** (re-escalated case): the bar does not apply — nothing to compute. `RE_GATE_OUTCOME="RE-ESCALATED"`. Skip to Phase M8.
|
|
239
|
+
|
|
240
|
+
**If `workflow:awaiting-merge` is present** (clean re-review case — the only branch where `review-pr.md`'s guard has already safely parked this PR): compute the bar.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
244
|
+
--jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]')
|
|
245
|
+
APPROVED_COUNT=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED_COUNT=${APPROVED_COUNT:-0}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Auto-land bar** (per #1809 Q1) — BOTH conditions required:
|
|
249
|
+
1. `APPROVED_COUNT >= 2` — at least two distinct adversarial `APPROVED:` review comments (same counting convention as `work-on.md` Phase 7A).
|
|
250
|
+
2. `GATE_PASSED = true` from this remediation's own Phase M3 quality-gate loop.
|
|
251
|
+
|
|
252
|
+
**If the bar is met**:
|
|
253
|
+
```bash
|
|
254
|
+
gh pr merge {PR_NUMBER} {GH_FLAG} --merge
|
|
255
|
+
MERGE_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state --jq '.state')
|
|
256
|
+
if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
257
|
+
RESOLUTION=$(resolve_script 'transition-label')
|
|
258
|
+
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
259
|
+
case "$TIER" in
|
|
260
|
+
adaptive|universal) bash "$SCRIPT_PATH" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
261
|
+
prose)
|
|
262
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" \
|
|
263
|
+
--remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
RE_GATE_OUTCOME="AUTO-LANDED"
|
|
267
|
+
else
|
|
268
|
+
RE_GATE_OUTCOME="HELD-AWAITING-MERGE"
|
|
269
|
+
# gh pr merge reported success but the PR isn't actually MERGED — leave workflow:awaiting-merge
|
|
270
|
+
# in place (unchanged) and let a human merge manually rather than retrying automatically.
|
|
271
|
+
fi
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**If the bar is NOT met**: leave the issue at `workflow:awaiting-merge` exactly as `review-pr.md`'s guard set it — do NOT attempt a merge. `RE_GATE_OUTCOME="HELD-AWAITING-MERGE"`. Fail-safe direction: any doubt about the bar defaults to holding, matching `review-pr.md`'s own existing default for every other caller.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Phase M8: Finalize FORGE:REMEDIATION Paper Trail
|
|
279
|
+
|
|
280
|
+
Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this is the single idempotency marker checked by Phase M0 (this file, on future resume) and by the orchestrator's item 6.4 dispatch guard:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
case "$RE_GATE_OUTCOME" in
|
|
284
|
+
AUTO-LANDED) AUTO_LAND_BAR_TEXT="MET (${APPROVED_COUNT:-0} APPROVED: reviews + quality gate pass)"; OUTCOME_DETAIL="to {PR_BASE}" ;;
|
|
285
|
+
HELD-AWAITING-MERGE) AUTO_LAND_BAR_TEXT="NOT MET (${APPROVED_COUNT:-0} APPROVED: reviews)"; OUTCOME_DETAIL="at workflow:awaiting-merge" ;;
|
|
286
|
+
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
287
|
+
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|
|
288
|
+
*) AUTO_LAND_BAR_TEXT="N/A"; OUTCOME_DETAIL="" ;;
|
|
289
|
+
esac
|
|
290
|
+
|
|
291
|
+
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
292
|
+
## Remediation Complete for PR #{PR_NUMBER}
|
|
293
|
+
|
|
294
|
+
**Findings addressed**: ${#ADDRESSED_FINDING_NUMBERS[@]} (${ADDRESSED_FINDING_NUMBERS[*]:-none})
|
|
295
|
+
**Re-review verdict**: ${RE_REVIEW_VERDICT:-unknown}
|
|
296
|
+
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
297
|
+
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
298
|
+
|
|
299
|
+
<!-- FORGE:REMEDIATION:COMPLETE -->"
|
|
300
|
+
|
|
301
|
+
gh pr comment {PR_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|
|
302
|
+
gh issue comment {ISSUE_NUMBER} {GH_FLAG} --body "$REMEDIATION_BODY"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**If the outcome was `AUTO-LANDED`**: this Skill invocation is itself the caller's terminal delegate (Phase 0A.1 of `work-on.md` already told its own routing loop to STOP after dispatching here) — so `remediate.md` must drive the close phase itself rather than assume some other inline logic will. Invoke the close subcommand directly, the same way `work-on/review.md` does when it hands off from a spawned sub-agent context:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Skill("work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --pr {PR_NUMBER} --base {PR_BASE}")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`work-on:close` handles project board update, final issue body, parent tracker, trajectory log, and worktree cleanup (including the remediation worktree at `{WORKTREE_PATH}`) — do not duplicate any of that here.
|
|
312
|
+
|
|
313
|
+
**If the outcome was `HELD-AWAITING-MERGE`, `RE-ESCALATED`, or `UNFIXABLE`**: leave the worktree in place (a human may need it for manual inspection/merge) and return the structured result below without invoking close. Do not close the issue.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Output
|
|
318
|
+
|
|
319
|
+
Return this structured block to the caller:
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
REMEDIATE_RESULT:
|
|
323
|
+
status: COMPLETE | ALREADY_DONE | UNFIXABLE | BLOCKED
|
|
324
|
+
pr_number: {PR_NUMBER}
|
|
325
|
+
issue_number: {ISSUE_NUMBER}
|
|
326
|
+
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | N/A
|
|
327
|
+
findings_addressed: [{finding_number}, ...]
|
|
328
|
+
blocker: {description if status=BLOCKED}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself. For every other `re_gate_outcome`, this result is terminal for the current invocation: the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`). Whether invoked standalone (`/work-on <pr> --remediate`) or via the orchestrator's item 6.4 dispatch, no further action is required from the caller.
|
|
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--worktree P
|
|
|
12
12
|
**Invoked by**: `work-on.md` Phase 4–5, after `build/validate.md` returns `GATE_PASSED: true`.
|
|
13
13
|
**Output**: Push branch, create PR, invoke /review-pr --auto-merge, return result to caller.
|
|
14
14
|
|
|
15
|
-
**Agent model policy**: `model: "
|
|
15
|
+
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, `FORGE:CHECKPOINT` writes) stay at this tier because they're interleaved with the review/merge-decision steps in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
|
|
16
16
|
**NEVER use plan mode (EnterPlanMode).**
|
|
17
17
|
|
|
18
18
|
<!-- FORGE:SPEC_LOADED — work-on/review.md loaded and active. Agent is bound by this spec. -->
|