forgedock 1.16.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +92 -0
- package/bin/engine/phases.mjs +86 -26
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +13 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +12 -1
- package/bin/engine.mjs +145 -40
- package/bin/forgedock.mjs +7 -1
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +187 -0
- package/bin/runner.mjs +7 -0
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +140 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +224 -0
- package/commands/cleanup.md +83 -2
- package/commands/deploy-info.md +8 -4
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +17 -2
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +102 -25
- package/commands/review-pr.md +119 -103
- package/commands/work-on/build/implement.md +26 -5
- package/commands/work-on/build/validate.md +3 -2
- package/commands/work-on/build.md +95 -64
- package/commands/work-on/close.md +17 -6
- package/commands/work-on/decompose.md +45 -4
- package/commands/work-on/investigate.md +6 -2
- package/commands/work-on/remediate.md +22 -6
- package/commands/work-on/review.md +2 -2
- package/docs/CONFIG.md +33 -0
- package/docs/OUTCOME-CODES.md +48 -0
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +62 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +13 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +54 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +5 -0
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +8 -3
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-pr-domain-map.test.sh +8 -3
- package/scripts/review-pr-gate2.test.sh +15 -0
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +65 -5
- package/scripts/review-prepare.sh +517 -0
- package/scripts/review-prepare.test.sh +270 -0
- package/scripts/review-roster.sh +172 -0
- package/scripts/review-roster.test.sh +163 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/wait-ci-green.sh +8 -1
- package/templates/forge.yaml.minimal +9 -0
|
@@ -141,21 +141,42 @@ Re-read current state before doing anything:
|
|
|
141
141
|
```bash
|
|
142
142
|
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
|
|
143
143
|
|
|
144
|
-
# Check investigation report
|
|
144
|
+
# Check investigation report — intentionally NOT cycle-scoped: investigate may commit CONFIRMED in a later cycle without re-posting INVESTIGATOR, so scoping it would block every new-cycle build
|
|
145
145
|
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh) # script resolution block verbatim; unresolved/failed read = fail closed
|
|
146
146
|
gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:INVESTIGATOR -->'
|
|
147
147
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
# Current-cycle slice (forge#3666): every resume read below runs on the comments AFTER the newest TRUSTED
|
|
149
|
+
# `<!-- FORGE:CYCLE: n -->` marker (same predicate as `cycleNumber`/`sliceToCurrentCycle` in bin/engine/phases.mjs:
|
|
150
|
+
# trusted author incl. the FORGE_TRAIL_TRUSTED_ASSOCIATIONS / FORGE_TRAIL_TRUSTED_LOGINS overrides, same `${VAR-default}`
|
|
151
|
+
# handling as scripts/trusted-comments.sh; body starts with the header, marker on its own line); all comments when no marker exists.
|
|
152
|
+
# A cycle-1 BUILDER:COMPLETE must never yield ALREADY_DONE in cycle 2. Re-declare this function verbatim in every
|
|
153
|
+
# fence that uses it (shell functions do not survive across fences).
|
|
154
|
+
cycle_slice() {
|
|
155
|
+
jq -s --arg assoc "${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}" --arg logins "${FORGE_TRAIL_TRUSTED_LOGINS-}" '[.[][]] as $c
|
|
156
|
+
| ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
|
|
157
|
+
| ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
|
|
158
|
+
| ([$c | to_entries[]
|
|
159
|
+
| select(((.value.author_association // "") as $x | $A | index($x) != null) or ((.value.user.type // "") == "Bot") or ((.value.user.login // "") as $x | $L | index($x) != null))
|
|
160
|
+
| select((.value.body // "") | test("\\A\\s*<!--\\s*FORGE:CYCLE:") and test("(?m)^[ \\t]*<!--[ \\t]*FORGE:CYCLE:[ \\t]*[0-9]+[ \\t]*-->[ \\t]*$"))
|
|
161
|
+
| .key] | last) as $i
|
|
162
|
+
| if $i == null then $c else $c[($i + 1):] end'
|
|
163
|
+
}
|
|
164
|
+
COMMENTS_JSON=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate) \
|
|
165
|
+
&& CYCLE_SLICE=$(printf '%s' "$COMMENTS_JSON" | cycle_slice) && [ -n "$CYCLE_SLICE" ] \
|
|
166
|
+
|| echo "CYCLE_LOOKUP=FAILED" # a failed read is NEVER "no marker"
|
|
167
|
+
|
|
168
|
+
# Check if build already completed (require FORGE:BUILDER:COMPLETE — not just FORGE:BUILDER); current-cycle slice only
|
|
169
|
+
printf '%s' "$CYCLE_SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:BUILDER'
|
|
151
170
|
|
|
152
|
-
# Existing classification from a prior run (resume path)
|
|
153
|
-
EXISTING_FAST_PATH=$(
|
|
154
|
-
|
|
171
|
+
# Existing classification from a prior run of THIS cycle (resume path); none in a fresh cycle, so B0.5 re-classifies and re-posts
|
|
172
|
+
EXISTING_FAST_PATH=$(printf '%s' "$CYCLE_SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:FAST_PATH' 2>/dev/null \
|
|
173
|
+
| head -n 1 | jq -r 'select(type == "string")')
|
|
155
174
|
```
|
|
156
175
|
|
|
176
|
+
`CYCLE_LOOKUP=FAILED` (or `trusted-comments.sh` unresolved) → Blocked exit with blocker `cycle-lookup-unavailable: could not read FORGE:CYCLE` (a transient GitHub failure uses the `github-unavailable:` prefix); never read a failed lookup as "no marker". Below, "present" and "exists" for BUILDER, BUILDER:COMPLETE, FAST_PATH, DIFF_SIZE, ACCEPTANCE_GATE and BUILD_BLOCKED comments always mean present in the **current-cycle slice** (comments after the newest trusted `FORGE:CYCLE` marker); a cycle-1 `BUILDER:COMPLETE` is invisible in cycle 2, so a new cycle with no slice COMPLETE falls through to B1 and builds fresh.
|
|
177
|
+
|
|
157
178
|
**Resume check**:
|
|
158
|
-
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted,
|
|
179
|
+
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment of the current-cycle slice → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted, comment of the current-cycle slice that starts with `FORGE:ACCEPTANCE_GATE` or `FORGE:BUILD_BLOCKED`, through the same anchored, trust-filtered lookup as the size-gate resume below (script resolution block verbatim; pipe `cycle_slice` into `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
159
180
|
- Lookup failed (`trusted-comments.sh` unresolved or `gh` error) → Blocked exit with blocker `gate-lookup-unavailable: could not read FORGE:ACCEPTANCE_GATE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no gate marker".
|
|
160
181
|
- Newest is `FORGE:ACCEPTANCE_GATE:FAILED`, `FORGE:ACCEPTANCE_GATE:BLOCKED`, or a `FORGE:BUILD_BLOCKED` comment → the gate is blocked and the engine fails closed on it, so returning ALREADY_DONE would deadlock every re-run. Derive BRANCH/WORKTREE_PATH/PR_BASE as in B1A/B1C (without creating anything), verify the branch and worktree exist and that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH (missing or mismatched → Blocked exit `worktree-missing: cannot re-enter B6.5`; never recreate silently), recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction, then skip B0.5-B6 and re-enter at **Phase B6.5**, which posts a fresh PASSED/FAILED/BLOCKED verdict (the one-repair `FORGE:ACCEPTANCE_REPAIR` bound still applies).
|
|
161
182
|
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED`, or no gate marker exists → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
@@ -165,7 +186,8 @@ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate
|
|
|
165
186
|
set -o pipefail
|
|
166
187
|
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
167
188
|
[ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ] || { echo "DIFF_SIZE_LOOKUP=FAILED (trusted-comments.sh unresolved)"; exit 0; }
|
|
168
|
-
|
|
189
|
+
# <cycle_slice function from B0, verbatim>
|
|
190
|
+
DS=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | cycle_slice | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE') \
|
|
169
191
|
|| { echo "DIFF_SIZE_LOOKUP=FAILED"; exit 0; }
|
|
170
192
|
LAST_RESULT=$(printf '%s\n' "$DS" | jq -r 'select(type == "string")' | sed -n 's/^result: *\([A-Z]*\).*/\1/p' | tail -n 1)
|
|
171
193
|
echo "DIFF_SIZE_RESULT=${LAST_RESULT:-<none>}"
|
|
@@ -173,13 +195,17 @@ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate
|
|
|
173
195
|
- `DIFF_SIZE_LOOKUP=FAILED` → Blocked exit with blocker `size-gate-unavailable: could not read FORGE:DIFF_SIZE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no DIFF_SIZE".
|
|
174
196
|
- `DIFF_SIZE_RESULT=OVER` or `DIFF_SIZE_RESULT=COHESIVE` → verify, without creating anything, that the branch and worktree exist (derive BRANCH/WORKTREE_PATH as in B1A/B1C), that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH, and that a staged diff is present (`! git -C "$WORKTREE_PATH" diff --cached --quiet`). Recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction. When all hold, skip B0.5-B5 and continue at B5.5 (it re-measures, then applies the override / `COHESIVE` / `NEEDS_DECOMPOSE` / `OK` logic; never re-run implement).
|
|
175
197
|
- Anything else (no `FORGE:DIFF_SIZE`, `result` is not `OVER`, worktree or branch missing or mismatched, nothing staged, band unrecoverable) → never skip ahead on missing state; fall through to the next rule.
|
|
176
|
-
- If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
|
|
198
|
+
- If `<!-- FORGE:BUILDER -->` exists in the current-cycle slice BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
|
|
199
|
+
Select with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR). Delete ONLY when exactly one non-complete candidate exists and it carries no other FORGE phase marker; unparsable, zero, or multiple matches delete nothing (fail closed). <!-- Added: forge#3668 -->
|
|
177
200
|
```bash
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
# <cycle_slice function from B0, verbatim> — only a current-cycle partial is ever deleted; prior-cycle comments are never touched
|
|
202
|
+
BLD=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | cycle_slice | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]') || BLD=""
|
|
203
|
+
PARTIAL_ID=$(printf '%s' "$BLD" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end' 2>/dev/null)
|
|
180
204
|
if [ -n "$PARTIAL_ID" ]; then
|
|
181
205
|
gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
|
|
182
206
|
echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build from Phase B2"
|
|
207
|
+
else
|
|
208
|
+
echo "builder resume: no single safe partial BUILDER comment — deleting nothing"
|
|
183
209
|
fi
|
|
184
210
|
```
|
|
185
211
|
- If there is no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → Blocked exit with blocker "Investigation not complete — run investigate first".
|
|
@@ -267,7 +293,7 @@ Whatever path was taken, set `COMPLEXITY_BAND` (uppercase) for the rest of this
|
|
|
267
293
|
|
|
268
294
|
### B1A: Derive branch name
|
|
269
295
|
|
|
270
|
-
If `--branch` was passed, or a local/remote branch already exists whose name ends in `-{NUMBER}` (`git branch -a --list "*-{NUMBER}"`), reuse it (resume). Otherwise derive from the issue title: lowercase, hyphenated, max 40 chars (truncate if needed).
|
|
296
|
+
If `--branch` was passed, or a local/remote branch already exists whose name ends in `-{NUMBER}` (`git branch -a --list "*-{NUMBER}"`), reuse it (resume). **New-cycle guard** <!-- Added: forge#3666 -->: when a trusted `FORGE:CYCLE` marker exists and the current-cycle slice has no `FORGE:BUILDER`, never re-adopt a prior-cycle branch whose PR is already merged or closed (`gh pr list {GH_FLAG} --head "<branch>" --state all --json state --jq '.[].state'` shows `MERGED`/`CLOSED`, or the branch is already merged into `origin/{PR_BASE}`); derive a fresh name instead (append `-c<cycle>` before `-{NUMBER}`, e.g. `fix/foo-c2-{NUMBER}`). Otherwise derive from the issue title: lowercase, hyphenated, max 40 chars (truncate if needed).
|
|
271
297
|
- Bug / fix issues → prefix `fix/`
|
|
272
298
|
- Feature issues → prefix `feat/`
|
|
273
299
|
- Refactor / maintenance → prefix `fix/` or `refactor/`
|
|
@@ -347,7 +373,7 @@ esac
|
|
|
347
373
|
|
|
348
374
|
**Skip if COMPLEXITY_BAND: INVESTIGATION** — go to B5. For every other band (TRIVIAL included) the contract is mandatory. If a `FORGE:CONTRACT` comment already exists on the issue (resume), do not post a second one — continue to B2.1.
|
|
349
375
|
|
|
350
|
-
Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why. Run B2.0 first: it files
|
|
376
|
+
Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why. Run B2.0 first: it folds small siblings and files one batched follow-up for the `deferred` out-of-scope items and validates the section, so `{OUT_OF_SCOPE_ITEMS}` below already holds real issue numbers.
|
|
351
377
|
|
|
352
378
|
### B2.0: Out-of-scope dispositions — file deferred follow-ups, validate the section <!-- Added: forge#3447 -->
|
|
353
379
|
|
|
@@ -365,51 +391,26 @@ Every item in the contract's `### Out of Scope` section must carry exactly one d
|
|
|
365
391
|
- **Known-flaw rule**: a contract that keeps a known-flawed design (a design the investigation or architect plan identified as flawed, a fail-open caller, an unsound threshold) must state the flaw and list it as a `deferred → #N` item with a linked follow-up. It may not hide behind `accepted-risk` or free text.
|
|
366
392
|
- Free-text exclusions without a disposition are invalid. `->` is accepted for `→`.
|
|
367
393
|
|
|
394
|
+
**Fold, then batch** <!-- Added: forge#3843 -->: before filing anything, fold small siblings into scope. A candidate deferred item that is the same mechanical change at a sibling path and fits the remaining size budget (estimate within `0.8 × build.diff_size.threshold`, `1000` when the threshold is `0` or unset, minus the current estimate) goes into the contract's Deliverables, not Out of Scope; count it as `folded`. Defer only genuinely separate work (different module, design or owner). All remaining deferred items of this build go into ONE follow-up issue with a checklist; `scripts/file-deferred-followups.sh` appends to an open trusted follow-up for the same parent (exact path match) or creates one.
|
|
395
|
+
|
|
368
396
|
**File the `deferred` follow-ups** (skip the filing on resume when the contract already exists):
|
|
369
397
|
|
|
370
398
|
```bash
|
|
371
399
|
# <Script resolution block from work-on/build.md, verbatim>
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
LOOKUP=$(gh api -X GET search/issues -f per_page=50 \
|
|
379
|
-
-f q="\"FORGE:DEFERRED_FROM: #{NUMBER}\" in:body repo:{GH_REPO} is:issue is:open" --jq '[.items[] | select(((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot"))]' 2>/dev/null) || LOOKUP="FAILED"
|
|
380
|
-
if [ "$LOOKUP" = "FAILED" ]; then
|
|
381
|
-
echo "DEFERRED_LOOKUP=FAILED" # a failed lookup is not "none found": Blocked exit, never re-file blind
|
|
382
|
-
else
|
|
383
|
-
DEFERRED_NUM=$(printf '%s' "$LOOKUP" | jq -r --arg p "$DEFERRED_PATH" \
|
|
384
|
-
'[.[] | select((.body // "") | contains($p))] | first | .number // empty' 2>/dev/null)
|
|
385
|
-
if [ -z "$DEFERRED_NUM" ]; then
|
|
386
|
-
DEFERRED_URL=$(run gh issue create {GH_FLAG} --title "follow-up: ${DEFERRED_PATH} (deferred from #{NUMBER})" \
|
|
387
|
-
--body "## Problem
|
|
388
|
-
|
|
389
|
-
${DEFERRED_WHY}
|
|
390
|
-
|
|
391
|
-
Deferred from #{NUMBER}: \`${DEFERRED_PATH}\` was declared out of scope in its contract and is tracked here instead of surfacing later as a review finding.
|
|
392
|
-
|
|
393
|
-
## Affected Files
|
|
394
|
-
|
|
395
|
-
- \`${DEFERRED_PATH}\`
|
|
396
|
-
|
|
397
|
-
## Acceptance Criteria
|
|
398
|
-
|
|
399
|
-
- [ ] The concern above is resolved for \`${DEFERRED_PATH}\`
|
|
400
|
-
|
|
401
|
-
${DEFERRED_MARKER}")
|
|
402
|
-
DEFERRED_NUM=$(printf '%s' "$DEFERRED_URL" | grep -oE '[0-9]+$')
|
|
403
|
-
[ -n "${DRY_RUN:-}" ] && DEFERRED_NUM="0"
|
|
404
|
-
fi
|
|
405
|
-
echo "DEFERRED_ISSUE=${DEFERRED_NUM:-}"
|
|
406
|
-
fi
|
|
400
|
+
# DEFERRED_TSV: one line per remaining deferred item: path<TAB>one-line why<TAB>known-flaw (or empty)
|
|
401
|
+
SCRIPT_REF=$(resolve_script 'file-deferred-followups')
|
|
402
|
+
case "$SCRIPT_REF" in
|
|
403
|
+
prose:*) echo "DEFERRED_SCRIPT=PROSE" ;; # file ONE issue by hand: marker + checklist of every path (see below)
|
|
404
|
+
*) printf '%s\n' "$DEFERRED_TSV" | bash "${SCRIPT_REF#*:}" --parent {NUMBER} --repo {GH_REPO} --folded "${FOLDED_COUNT:-0}"; echo "DEFERRED_RC=$?" ;;
|
|
405
|
+
esac
|
|
407
406
|
```
|
|
408
407
|
|
|
409
|
-
-
|
|
410
|
-
-
|
|
411
|
-
-
|
|
412
|
-
-
|
|
408
|
+
- Output is one `path<TAB>issue` row per item (rows share the batched issue number: use it as the `#N` in each `deferred → #N` bullet) plus a `DEFERRED_TELEMETRY deferred_items=N folded=N batched_into=#M filed=N` line (forwarded to the #3834 emitter when present).
|
|
409
|
+
- A known-flawed design is recorded as a `known-flaw` checklist item in that batched issue (pass the flag in the TSV); it is never dropped or hidden behind `accepted-risk`.
|
|
410
|
+
- Only follow-ups by a trusted author (OWNER/MEMBER/COLLABORATOR or Bot; `FORGE_TRAIL_TRUSTED_*` honoured) are reused; an untrusted match is ignored.
|
|
411
|
+
- `DEFERRED_LOOKUP=FAILED` or `DEFERRED_FILE=FAILED` (non-zero `DEFERRED_RC`) → Blocked exit with blocker `github-unavailable: could not file or look up deferred follow-ups` (a transient failure follows the retry rule above; no `needs-human` for `github-unavailable:`).
|
|
412
|
+
- Prose tier (script absent): create one issue by hand whose body has the exact `<!-- FORGE:DEFERRED_FROM: #{NUMBER} -->` marker and a `- [ ] \`path\` — why` checklist line per path, after a trusted-author marker lookup; a failed lookup is a Blocked exit.
|
|
413
|
+
- On resume the lookup reuses the open issue carrying the marker and appends only missing paths, so a retried build never files a duplicate.
|
|
413
414
|
|
|
414
415
|
**Validate the composed section** with `scripts/check-contract-scope.sh validate` (the exact text that will replace `{OUT_OF_SCOPE_ITEMS}`):
|
|
415
416
|
|
|
@@ -655,6 +656,7 @@ if [ -n "$JUSTIFICATION" ]; then echo "OVERRIDE=VALID"; else echo "OVERRIDE=NONE
|
|
|
655
656
|
**Step 3 — record.** Set `DIFF_LINES`, `EXCLUDED_LINES`, `THRESHOLD`, `RESULT` (`OK`, `OVERRIDDEN`, `COHESIVE` or `OVER`), `SPLIT_PROPOSAL` and `COHESION_NOTE` (set only for `COHESIVE`) from the steps above, then post one `<!-- FORGE:DIFF_SIZE -->` comment, idempotently: find the latest trusted comment starting with the marker and edit it, otherwise create it (on resume never stack duplicates; readers use the latest trusted one):
|
|
656
657
|
|
|
657
658
|
```bash
|
|
659
|
+
# <Script resolution block from above goes here, verbatim>
|
|
658
660
|
BODY="<!-- FORGE:DIFF_SIZE -->
|
|
659
661
|
## Diff Size
|
|
660
662
|
|
|
@@ -663,6 +665,7 @@ excluded_lines: ${EXCLUDED_LINES}
|
|
|
663
665
|
threshold: ${THRESHOLD}
|
|
664
666
|
result: ${RESULT}
|
|
665
667
|
${ESTIMATE_LINE:-}${COHESION_NOTE:-}${SPLIT_PROPOSAL:-}"
|
|
668
|
+
_t=$(forge_resolve lib/telemetry.sh); FORGE_REPO="{GH_REPO}" FORGE_ISSUE="{NUMBER}" FORGE_PHASE=build bash "${_t:-/dev/null}" diff_size lines="$DIFF_LINES" excluded="$EXCLUDED_LINES" threshold="$THRESHOLD" result="$RESULT" >/dev/null 2>&1 || true # forge#3834 telemetry (final result incl. OVERRIDDEN)
|
|
666
669
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
667
670
|
CID=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" --jq '[.[] | select(.body | startswith("<!-- FORGE:DIFF_SIZE")) | select((.author_association | IN("OWNER","MEMBER","COLLABORATOR")) or (.user.type == "Bot"))] | last | .id // ""' 2>/dev/null || true)
|
|
668
671
|
if [ -n "$CID" ]; then run gh api "repos/{GH_REPO}/issues/comments/${CID}" -X PATCH -f body="$BODY" >/dev/null 2>&1 || run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"
|
|
@@ -791,6 +794,8 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
|
|
|
791
794
|
**Otherwise — execute each check**: <!-- Added: forge#1829 -->
|
|
792
795
|
|
|
793
796
|
```bash
|
|
797
|
+
# <Script resolution block, verbatim>
|
|
798
|
+
GREP_CLS=$(forge_resolve classify-grep-exit.sh) # empty/unresolved = every grep exit>=2 is a real FAIL (fail closed)
|
|
794
799
|
GATE_PASS=true
|
|
795
800
|
CORRECTED=""
|
|
796
801
|
FAILED_CHECKS=""
|
|
@@ -798,6 +803,8 @@ VERIFIED_COUNT=0 # checks actually executed and judged (PASS or FAIL); mal
|
|
|
798
803
|
MALFORMED_CHECKS="" # malformed-check: generated check that can never pass; reported, never repaired, never needs-human
|
|
799
804
|
DEFERRED_COUNT=0 # phase=review checks (and legacy "no pull requests found" results): evaluated by review R2.5, never counted here
|
|
800
805
|
DEFERRED_CHECKS="" # <!-- Added: forge#3793 -->
|
|
806
|
+
MANUAL_CHECKS="" # per-check type=skipped (manual / post-deploy): never executed, listed as unchecked follow-ups <!-- Added: forge#3825 -->
|
|
807
|
+
SPEC_DEFECT=false # nothing verifiable: a spec defect that code repair cannot fix <!-- Added: forge#3825 -->
|
|
801
808
|
|
|
802
809
|
while IFS= read -r check_line; do
|
|
803
810
|
# Fields are extracted by anchoring each sed pattern to `^ACCEPTANCE_CHECK:` and walking
|
|
@@ -837,7 +844,7 @@ while IFS= read -r check_line; do
|
|
|
837
844
|
PHASE=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("(([^"\\]|\\.)*)"|[^ ]*) matcher=("(([^"\\]|\\.)*)"|[^ ]*) phase=(build|review)( .*)?$/\7/p')
|
|
838
845
|
[ "$PHASE" = "review" ] || PHASE=build
|
|
839
846
|
|
|
840
|
-
[ "$TYPE" = "skipped" ] && continue
|
|
847
|
+
[ "$TYPE" = "skipped" ] && { MANUAL_CHECKS="${MANUAL_CHECKS}\n- [ ] **$ID** ($DESC)"; continue; }
|
|
841
848
|
|
|
842
849
|
# Deferred (review) <!-- Added: forge#3793 -->: never executed, never VERIFIED, never GATE_PASS=false, never in FAILED_CHECKS / --fix-acceptance.
|
|
843
850
|
if [ "$PHASE" = "review" ]; then DEFERRED_COUNT=$((DEFERRED_COUNT+1)); DEFERRED_CHECKS="${DEFERRED_CHECKS}\n- **$ID** ($DESC): DEFERRED (review)"; continue; fi
|
|
@@ -846,6 +853,7 @@ while IFS= read -r check_line; do
|
|
|
846
853
|
# check defect, not an implementation failure. Static detection only parses (bash -n), never executes.
|
|
847
854
|
MALFORMED=""
|
|
848
855
|
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && { case "$TARGET" in *'\\\\'*) MALFORMED="run of 4+ backslashes (over-escaped target)";; esac; [ -n "$MALFORMED" ] || bash -n -c "$TARGET" 2>/dev/null || MALFORMED="shell syntax error (unbalanced quotes)"; }
|
|
856
|
+
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && [ -z "$MALFORMED" ] && case "$(printf '%s' "$TARGET" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" in none) MALFORMED='target "none" is not a command (a manual criterion must be type=skipped)';; esac
|
|
849
857
|
if [ -n "$MALFORMED" ]; then MALFORMED_CHECKS="${MALFORMED_CHECKS}\n- **$ID** ($DESC): $MALFORMED"; continue; fi
|
|
850
858
|
|
|
851
859
|
RESULT="PASS"
|
|
@@ -868,12 +876,14 @@ while IFS= read -r check_line; do
|
|
|
868
876
|
if [ "$MATCHER" = "exit_0" ]; then
|
|
869
877
|
ERR=$(eval "$TARGET" 2>&1 >/dev/null); RC=$?
|
|
870
878
|
# grep exit >= 2 is a usage/syntax error (e.g. a pattern starting with `-`), never a verdict on the code; exit 1 stays FAIL.
|
|
871
|
-
#
|
|
872
|
-
[ "$RC" -ge 2 ] &&
|
|
879
|
+
# Malformed only when stderr positively names a usage error (classify-grep-exit.sh); empty stderr (`2>/dev/null`, `-s`) or any operand error is a REAL failure.
|
|
880
|
+
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
881
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -qF 'command not found' && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
873
882
|
[ "$RC" -eq 0 ] || [ -n "$MALFORMED" ] || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
|
|
874
883
|
else
|
|
875
|
-
OUTPUT=$(eval "$TARGET" 2
|
|
876
|
-
[ "$RC" -ge 2 ] &&
|
|
884
|
+
ERRF=$(mktemp); OUTPUT=$(eval "$TARGET" 2>"$ERRF"); RC=$?; ERR=$(cat "$ERRF"); rm -f "$ERRF"; [ -z "$ERR" ] || OUTPUT="${OUTPUT}${OUTPUT:+$'\n'}${ERR}" # classify on stderr only, match on both
|
|
885
|
+
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
886
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -qF 'command not found' && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
877
887
|
if [ -n "$MALFORMED" ]; then :
|
|
878
888
|
elif ! printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER"; then
|
|
879
889
|
if printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER"; then CORRECTED="${CORRECTED}\n- **$ID**: output matched as a literal string"
|
|
@@ -888,7 +898,7 @@ while IFS= read -r check_line; do
|
|
|
888
898
|
|
|
889
899
|
if [ -n "$MALFORMED" ]; then MALFORMED_CHECKS="${MALFORMED_CHECKS}\n- **$ID** ($DESC): $MALFORMED"; continue; fi
|
|
890
900
|
|
|
891
|
-
# Legacy fallback (
|
|
901
|
+
# Legacy fallback (any phase tag): a gh pr check that finds no PR is waiting on the review phase, not failing.
|
|
892
902
|
if [ "$RESULT" = "FAIL" ] && printf '%s' "$ERR" | grep -qF 'no pull requests found'; then
|
|
893
903
|
case "$TARGET" in *'gh pr'*) DEFERRED_COUNT=$((DEFERRED_COUNT+1)); DEFERRED_CHECKS="${DEFERRED_CHECKS}\n- **$ID** ($DESC): DEFERRED (review) (no pull request exists yet)"; continue;; esac
|
|
894
904
|
fi
|
|
@@ -903,21 +913,38 @@ done <<< "$ACCEPTANCE_CHECKS"
|
|
|
903
913
|
# A gate that verified nothing must not pass: with every check skipped as malformed, PASSED would be a green light on zero evidence.
|
|
904
914
|
# A spec whose checks are all deferred to review (DEFERRED_COUNT > 0) is not vacuous: review R2.5 evaluates them before merge.
|
|
905
915
|
if [ "$VERIFIED_COUNT" -eq 0 ] && [ "$DEFERRED_COUNT" -eq 0 ]; then
|
|
916
|
+
# spec-defect <!-- Added: forge#3825 -->: only the spec is wrong, so this is a BLOCKED verdict that never enters the --fix-acceptance repair (#3774).
|
|
906
917
|
GATE_PASS=false
|
|
907
|
-
|
|
918
|
+
SPEC_DEFECT=true
|
|
908
919
|
fi
|
|
909
920
|
```
|
|
910
921
|
|
|
911
922
|
**Post gate result comment**:
|
|
912
923
|
|
|
913
924
|
```bash
|
|
925
|
+
# <Script resolution block, verbatim>
|
|
914
926
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
915
|
-
|
|
927
|
+
_nf=$(printf '%b' "$FAILED_CHECKS" | grep -c '^- ' || true); _nm=$(printf '%b' "$MALFORMED_CHECKS" | grep -c '^- ' || true); _t=$(forge_resolve lib/telemetry.sh)
|
|
928
|
+
[ -n "${DRY_RUN:-}" ] || FORGE_REPO="{GH_REPO}" FORGE_ISSUE="{NUMBER}" FORGE_PHASE=build bash "${_t:-/dev/null}" acceptance_gate phase=build verified="$VERIFIED_COUNT" failed="${_nf:-0}" malformed="${_nm:-0}" deferred="$DEFERRED_COUNT" >/dev/null 2>&1 || true # forge#3834 telemetry
|
|
929
|
+
if [ "$SPEC_DEFECT" = "true" ]; then
|
|
930
|
+
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
931
|
+
## Acceptance Gate — BLOCKED (spec-defect)
|
|
932
|
+
|
|
933
|
+
No acceptance check could be verified: every check was malformed, manual, or empty. The implementation is not at fault and a code repair cannot fix this, so the repair step is skipped.
|
|
934
|
+
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check**:%b\n' "$MALFORMED_CHECKS" )
|
|
935
|
+
$( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not verified at build time)**:%b\n' "$MANUAL_CHECKS" )
|
|
936
|
+
|
|
937
|
+
**Gate result: BLOCKED** — regenerate the acceptance spec by re-running \`/work-on:investigate {NUMBER}\`, then retry the build.
|
|
938
|
+
|
|
939
|
+
<!-- FORGE:ACCEPTANCE_GATE:BLOCKED -->"
|
|
940
|
+
run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
941
|
+
elif [ "$GATE_PASS" = "true" ]; then
|
|
916
942
|
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
917
943
|
## Acceptance Gate — PASSED
|
|
918
944
|
|
|
919
945
|
All machine-checkable build-phase acceptance criteria verified against real behavior.
|
|
920
946
|
$( [ -n "$DEFERRED_CHECKS" ] && printf '\n**Deferred to review (phase=review; evaluated against the PR before merge)**:%b\n' "$DEFERRED_CHECKS" )
|
|
947
|
+
$( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not verified at build time, confirm after merge/deploy)**:%b\n' "$MANUAL_CHECKS" )
|
|
921
948
|
$( [ -n "$CORRECTED" ] && printf '\n**Checks corrected (malformed matcher, literal text present)**:%b\n' "$CORRECTED" )
|
|
922
949
|
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS" )
|
|
923
950
|
|
|
@@ -938,11 +965,15 @@ Merge is blocked. Fix the failing criteria and re-run the validate phase.
|
|
|
938
965
|
fi
|
|
939
966
|
```
|
|
940
967
|
|
|
941
|
-
A `malformed-check` (a command/behavior target with a run of 4+ backslashes or a `bash -n` syntax error, or a grep target exiting >= 2 for a reason other than a missing/unreadable operand file, which stays a real FAIL) is reported in the comment and never sets `GATE_PASS=false` by itself, never enters `FAILED_CHECKS`, is never passed to `--fix-acceptance`, and never adds `needs-human` by itself; the gate still posts exactly one of PASSED/FAILED. The gate never posts PASSED with zero verified checks: if every check was malformed (`VERIFIED_COUNT=0` and `DEFERRED_COUNT=0`) it posts
|
|
968
|
+
A `malformed-check` (a command/behavior target with a run of 4+ backslashes or a `bash -n` syntax error, or a grep target exiting >= 2 for a reason other than a missing/unreadable operand file, which stays a real FAIL) is reported in the comment and never sets `GATE_PASS=false` by itself, never enters `FAILED_CHECKS`, is never passed to `--fix-acceptance`, and never adds `needs-human` by itself; the gate still posts exactly one of PASSED/FAILED. The gate never posts PASSED with zero verified checks: if every check was malformed (`VERIFIED_COUNT=0` and `DEFERRED_COUNT=0`) it is a spec-defect: it posts `FORGE:ACCEPTANCE_GATE:BLOCKED`, adds `needs-human`, prints `BUILD_RESULT: status: BLOCKED` (blocker: "regenerate acceptance spec via investigate") and skips the `--fix-acceptance` repair entirely, because implement cannot fix a spec defect. <!-- Added: forge#3694, forge#3825 -->
|
|
969
|
+
|
|
970
|
+
Also `malformed-check` <!-- Added: forge#3825 -->: a `command`/`behavior` target that is literally `none` (any case), or that exits 127 with `command not found` on stderr. Classification keys on that stderr text, never on the code alone: a missing script (a `bash` run of a nonexistent script path, with `No such file or directory`) also exits 127 and stays a real FAIL, so the missing-operand class is not reopened. A per-check `type=skipped` is never executed, never counted, and is listed under "Manual follow-up" as unchecked items in the PASSED comment.
|
|
971
|
+
|
|
972
|
+
A `phase=review` check (or, for legacy specs, tagged or not, a `command`/`behavior` check on a `gh pr` target whose output contains `no pull requests found`) is recorded as `DEFERRED (review)` in the PASSED comment: it is never executed here, never counted in `VERIFIED_COUNT`, never sets `GATE_PASS=false`, never enters `FAILED_CHECKS` or `--fix-acceptance`, and never spends the repair. A spec whose checks are all deferred posts PASSED (it is not "No Spec Found"). Review R2.5 evaluates the deferred checks against the real PR and blocks merge on failure. <!-- Added: forge#3793 -->
|
|
942
973
|
|
|
943
|
-
|
|
974
|
+
If `SPEC_DEFECT = true`: leave `needs-human`, print `BUILD_RESULT: status: BLOCKED`, blocker: "regenerate acceptance spec via investigate (spec-defect: no check could be verified)", and do NOT run the repair below.
|
|
944
975
|
|
|
945
|
-
If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
|
|
976
|
+
If `GATE_PASS = false` and `SPEC_DEFECT` is not `true`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
|
|
946
977
|
|
|
947
978
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
948
979
|
1.5. **Decomposition guard** <!-- Added: forge#3543 -->: read the `### Decomposition Assessment` of the latest FORGE:INVESTIGATOR comment. If it is `**YES**`, a failing check that belongs to a sub-item the assessment assigns to a separate issue (named in the check's `description`/`# item-N` tag, or matching a listed sub-issue title) is NOT repaired: exclude it from the `--fix-acceptance` list. When ambiguous, treat the check as decomposed-out (fail safe). If every remaining failure is decomposed-out, do not invoke implement: leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "acceptance check {ID} belongs to decomposed item {N} — repair refuses to implement work the investigator assigned to a separate issue".
|
|
@@ -57,7 +57,7 @@ PR_BASE="{PR_BASE}"; BRANCH="{BRANCH}"; WORKTREE_PATH="{WORKTREE_PATH}"; TERMINA
|
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| `merged` | C0 → C6, all phases | — |
|
|
59
59
|
| `investigation` | C0, C0.5, C1, C1.5, C2, C3, C4, C4.5, C5, C5.1–C5.4, C6 | C1.7, C5.5 (no PR); PR lines in the issue body, report and trajectory use the "no PR" form |
|
|
60
|
-
| `decomposed` | C0, C0.5, C4, C4.5, C5, C5.1–C5.4, C6 | C1, C1.7, C1.5, C2, C3, C5.5 — the parent
|
|
60
|
+
| `decomposed` | C0, C0.5, C4, C4.5, C5, C5.1–C5.4, C6 | C1, C1.7, C1.5, C2, C3, C5.5 — the parent keeps `workflow:decomposed` (owned by the decompose phase); it stays OPEN unless `decomposition.close_parent` closed it at decomposition, and C3 of the last child then completes it |
|
|
61
61
|
| `invalid` | C0, C0.5, C4, C4.5, C5, C6 | C1, C1.7, C1.5, C2, C3, C5.1–C5.5 — the issue was already closed with `workflow:invalid` by the investigate phase |
|
|
62
62
|
|
|
63
63
|
Every `gh pr view {PR_NUMBER}` and every other PR-dependent step in this file is guarded on a non-empty `PR_NUMBER`: with an empty PR, the PR-derived values render as `—` and nothing aborts.
|
|
@@ -824,15 +824,26 @@ OPEN_SUBS=$(printf '%s\n' "$UPDATED_PARENT" | grep -cE '^[[:space:]]*[-*+] \[ \]
|
|
|
824
824
|
OPEN_SUBS=${OPEN_SUBS:-0}
|
|
825
825
|
```
|
|
826
826
|
|
|
827
|
-
If `OPEN_SUBS == 0` (all sub-issues checked off)
|
|
827
|
+
If `OPEN_SUBS == 0` (all sub-issues checked off), branch on the parent's `state` <!-- Added: forge#3736 -->. An OPEN parent is closed and transitioned to `merged` with the same tiered dispatch and FULL remove list as C2. A CLOSED parent (closed at decomposition by `decomposition.close_parent`) is never closed a second time and never reopened: post the completion comment (once) and set `merged`:
|
|
828
828
|
```bash
|
|
829
829
|
if [ "$OPEN_SUBS" -eq 0 ]; then
|
|
830
|
-
gh issue
|
|
831
|
-
|
|
830
|
+
PARENT_STATE=$(gh issue view "$PARENT_REF" {GH_FLAG} --json state --jq '.state' 2>/dev/null || echo OPEN)
|
|
831
|
+
COMPLETE_MSG="All sub-issues complete. Closing parent tracker. Last completed: #{NUMBER}${PR_NUMBER:+ (PR #${PR_NUMBER})}."
|
|
832
|
+
if [ "$PARENT_STATE" = "CLOSED" ]; then
|
|
833
|
+
ALREADY_POSTED=$(gh api "repos/{GH_REPO}/issues/${PARENT_REF}/comments" \
|
|
834
|
+
--jq '[.[] | select(.body | startswith("All sub-issues complete"))] | length' 2>/dev/null || echo 0)
|
|
835
|
+
[ "${ALREADY_POSTED:-0}" -gt 0 ] || gh issue comment "$PARENT_REF" {GH_FLAG} --body "${COMPLETE_MSG/Closing parent tracker. /}" # allowlist:check-command-side-effects
|
|
836
|
+
set_workflow_state "$PARENT_REF" merged
|
|
837
|
+
else
|
|
838
|
+
gh issue close "$PARENT_REF" {GH_FLAG} --comment "$COMPLETE_MSG" # allowlist:check-command-side-effects
|
|
839
|
+
set_workflow_state "$PARENT_REF" merged
|
|
840
|
+
fi
|
|
832
841
|
PARENT_CLOSED=true
|
|
833
842
|
fi
|
|
834
843
|
```
|
|
835
844
|
|
|
845
|
+
With `decomposition.close_parent: false` (default) the parent is OPEN at this point, so behaviour is unchanged.
|
|
846
|
+
|
|
836
847
|
Set `PARENT_STATUS="✅ Complete"` and `PARENT_NOTES="Checked off in #${PARENT_REF}"` when the parent was updated.
|
|
837
848
|
|
|
838
849
|
---
|
|
@@ -1068,8 +1079,8 @@ case "$TERMINAL_STATE" in
|
|
|
1068
1079
|
PHASE2_ROW="✅ Decomposed | Sub-issues spawned (see FORGE:DECOMPOSED)"
|
|
1069
1080
|
BUILD_ROW="⏭ Skipped | Handled by sub-issues"
|
|
1070
1081
|
GATE_CELL="⏭ N/A |"; VERIF_CELL="⏭ N/A |"; REVIEW_CELL="⏭ N/A (no PR) |"
|
|
1071
|
-
CLOSE_CELL="✅ Complete |
|
|
1072
|
-
DECISIONS_BLOCK="- Decomposed into sub-issues; this issue
|
|
1082
|
+
CLOSE_CELL="✅ Complete | Tracker for sub-issues (stays open unless decomposition.close_parent)"
|
|
1083
|
+
DECISIONS_BLOCK="- Decomposed into sub-issues; this issue is the tracker (open, or closed at decomposition when `decomposition.close_parent` is true)" ;;
|
|
1073
1084
|
invalid)
|
|
1074
1085
|
PHASE2_ROW="⏭ Skipped | Issue invalid"
|
|
1075
1086
|
BUILD_ROW="⏭ Skipped | Issue invalid"
|
|
@@ -18,7 +18,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
**Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
|
|
21
|
-
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed
|
|
21
|
+
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`, close the parent when `decomposition.close_parent` is true (D6.5). STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
|
|
22
22
|
|
|
23
23
|
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on `<!-- DECOMPOSE:YES -->` (or a `**YES**` verdict under `### Decomposition Assessment`) in the newest trusted FORGE:INVESTIGATOR report, instead of terminating in place, so a headless `runIssue()` walk dispatches this subcommand rather than stopping short.
|
|
24
24
|
|
|
@@ -217,7 +217,7 @@ SUB_LIST=$(printf '%s\n' "$DECOMP_BODY" | sed -nE 's/^- #([0-9]+).*/\1/p' | past
|
|
|
217
217
|
echo "sub_issues: [${SUB_LIST}]"
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
Run Phase D6 (idempotent label re-assert), then EXIT with `DECOMPOSE_RESULT: status: ALREADY_DONE`, `sub_issues: [${SUB_LIST}]`, `comment_url` from the comment.
|
|
220
|
+
Run Phase D6 (idempotent label re-assert) and Phase D6.5 (close parent when `decomposition.close_parent` is true), then EXIT with `DECOMPOSE_RESULT: status: ALREADY_DONE`, `sub_issues: [${SUB_LIST}]`, `comment_url` from the comment.
|
|
221
221
|
|
|
222
222
|
---
|
|
223
223
|
|
|
@@ -283,7 +283,7 @@ CANONICAL_SUB_ISSUES=$(jq --arg parent "**Parent**: #{NUMBER}" --argjson expecte
|
|
|
283
283
|
CANONICAL_NUMBERS=$(printf '%s' "$CANONICAL_SUB_ISSUES" | jq -r 'map(.number) | sort | map(tostring) | join(", ")' 2>/dev/null)
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
-
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, run Phase D6 (idempotent label re-assert), and EXIT with:
|
|
286
|
+
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, run Phase D6 (idempotent label re-assert) and Phase D6.5 (close parent when `decomposition.close_parent` is true), and EXIT with:
|
|
287
287
|
|
|
288
288
|
```
|
|
289
289
|
DECOMPOSE_RESULT:
|
|
@@ -493,7 +493,7 @@ ${ATTRIBUTION_LINE}
|
|
|
493
493
|
|
|
494
494
|
## Phase D6: Update Labels
|
|
495
495
|
|
|
496
|
-
Decompose is the SOLE owner of the `workflow:decomposed` label. Transition with tiered dispatch; a transition to `decomposed` removes every other workflow label. Also run this block (idempotently) on both ALREADY_DONE exits.
|
|
496
|
+
Decompose is the SOLE owner of the `workflow:decomposed` label. Transition with tiered dispatch; a transition to `decomposed` removes every other workflow label. Also run this block (idempotently) on both ALREADY_DONE exits, followed by D6.5.
|
|
497
497
|
|
|
498
498
|
```bash
|
|
499
499
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
@@ -509,10 +509,51 @@ esac
|
|
|
509
509
|
|
|
510
510
|
---
|
|
511
511
|
|
|
512
|
+
## Phase D6.5: Close Parent (opt-in)
|
|
513
|
+
|
|
514
|
+
<!-- Added: forge#3736 -->
|
|
515
|
+
A decomposed parent is only a tracker, but while it is open it inflates GitHub's open-issue count. When `decomposition.close_parent` is `true`, close the parent now that the sub-issues exist, the `FORGE:DECOMPOSED:COMPLETE` comment is posted (D5) and `workflow:decomposed` is set (D6). The label is kept. Close C3 keeps checking children off the closed parent and posts the completion comment when the last one closes.
|
|
516
|
+
|
|
517
|
+
Run this phase on the normal path and on both ALREADY_DONE exits (D1 resume, D2.5). Set `SUB_NUMBERS` to the sub-issue numbers: `CREATED_NUMBERS` on the normal path, `SUB_LIST` (D1) or `CANONICAL_NUMBERS` (D2.5) on the resume paths, as a space or comma separated list. Config read is fail-safe: an unreadable `forge.yaml`, missing `yq` or any value other than `true` leaves the parent open.
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
521
|
+
CLOSE_PARENT=$(yq '.decomposition.close_parent // false' forge.yaml 2>/dev/null || echo false)
|
|
522
|
+
[ "$CLOSE_PARENT" = "true" ] || CLOSE_PARENT=false
|
|
523
|
+
if [ "$CLOSE_PARENT" = "true" ]; then
|
|
524
|
+
SUB_NUMBERS=$(printf '%s' "${SUB_NUMBERS:-}" | tr -c '0-9\n' ' ')
|
|
525
|
+
|
|
526
|
+
# Child guard: every child body must carry the parent reference Close C3 matches on.
|
|
527
|
+
for SUB_NUMBER in $SUB_NUMBERS; do
|
|
528
|
+
[ -n "$SUB_NUMBER" ] || continue
|
|
529
|
+
SUB_BODY=$(gh issue view "$SUB_NUMBER" {GH_FLAG} --json body --jq '.body' 2>/dev/null || true)
|
|
530
|
+
[ -n "$SUB_BODY" ] || continue
|
|
531
|
+
printf '%s\n' "$SUB_BODY" | grep -qE '^\*\*Parent\*\*: #{NUMBER}([^0-9]|$)' && continue
|
|
532
|
+
run gh issue edit "$SUB_NUMBER" {GH_FLAG} --body "$(printf '**Parent**: #{NUMBER}\n\n%s' "$SUB_BODY")" # allowlist:check-command-side-effects
|
|
533
|
+
done
|
|
534
|
+
|
|
535
|
+
# Close only an OPEN parent, and only once the DECOMPOSED:COMPLETE marker is on it.
|
|
536
|
+
PARENT_STATE=$(gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state' 2>/dev/null || true)
|
|
537
|
+
HAS_COMPLETE=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
538
|
+
--jq '[.[] | select(.body | contains("FORGE:DECOMPOSED:COMPLETE"))] | length' 2>/dev/null || echo 0)
|
|
539
|
+
if [ "$PARENT_STATE" = "OPEN" ] && [ "${HAS_COMPLETE:-0}" -gt 0 ]; then
|
|
540
|
+
REFS=$(for n in $SUB_NUMBERS; do printf '#%s ' "$n"; done)
|
|
541
|
+
run gh issue close {NUMBER} {GH_FLAG} --reason completed \
|
|
542
|
+
--comment "Decomposed into ${REFS}; tracked here until all complete." # allowlist:check-command-side-effects
|
|
543
|
+
fi
|
|
544
|
+
fi
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
If the close fails, do not fail the phase: the parent stays open (today's behaviour) and the decomposition is still complete. A closed parent keeps `workflow:decomposed`; `--retry` and engine resume refuse closed issues, which is acceptable because a decomposed parent has nothing left to run.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
512
551
|
## Phase D7: STOP
|
|
513
552
|
|
|
514
553
|
Decomposition is a terminal route for the parent issue. Each sub-issue will be picked up separately by /work-on.
|
|
515
554
|
|
|
555
|
+
Phase D6.5 has already run (with `SUB_NUMBERS="${CREATED_NUMBERS}"`) before this point.
|
|
556
|
+
|
|
516
557
|
Print the result block (see Result emission) as the final reply, with `status: COMPLETE`, `sub_issues: [${CREATED_NUMBERS}]` (numbers only) and `comment_url: ${COMMENT_URL}`.
|
|
517
558
|
|
|
518
559
|
**Router behavior after DECOMPOSE_RESULT**: `break` — do not continue to build/review/close for the parent issue.
|
|
@@ -706,8 +706,8 @@ ESTIMATE_FACTOR: {FACTOR — resolved build.diff_size.estimate_factor, positive
|
|
|
706
706
|
**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.
|
|
707
707
|
|
|
708
708
|
```
|
|
709
|
-
ACCEPTANCE_CHECK: id={ac-1} type={exists|contains|command|behavior} target="{file_path|command|url}" matcher="{string|exit_0|regex}" phase={build|review} description={one-line human description}
|
|
710
|
-
ACCEPTANCE_CHECK: id={ac-2} type={exists|contains|command|behavior} target="{file_path|command|url}" matcher="{string|exit_0|regex}" phase={build|review} description={one-line human description}
|
|
709
|
+
ACCEPTANCE_CHECK: id={ac-1} type={exists|contains|command|behavior|skipped} target="{file_path|command|url}" matcher="{string|exit_0|regex}" phase={build|review} description={one-line human description}
|
|
710
|
+
ACCEPTANCE_CHECK: id={ac-2} type={exists|contains|command|behavior|skipped} target="{file_path|command|url}" matcher="{string|exit_0|regex}" phase={build|review} description={one-line human description}
|
|
711
711
|
```
|
|
712
712
|
|
|
713
713
|
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):
|
|
@@ -726,6 +726,10 @@ ACCEPTANCE_CHECK: id=ac-7 type=command target="gh pr view --json body --jq .body
|
|
|
726
726
|
|
|
727
727
|
**Literal shell (MANDATORY)** <!-- Added: forge#3694 -->: a `command`/`behavior` `target` is literal shell, executed as written after only `\"` is unescaped. Never JSON-escape it: do not double backslashes (a run of four or more backslashes is flagged `malformed-check` by the acceptance gate and the check is never run). Pass grep patterns with `-e` or `--` so a pattern starting with `-` is not read as an option (exit 2 is also reported as `malformed-check`). For fixed strings prefer `grep -F`: `grep -qF -e '<literal>' <file>` needs no regex escaping at all.
|
|
728
728
|
|
|
729
|
+
**Manual and post-deploy criteria** <!-- Added: forge#3825 -->: a criterion that needs production, promotion/deploy, or human judgment cannot be verified at build time. Emit it per check as `ACCEPTANCE_CHECK: id=ac-N type=skipped target="none" matcher="none" phase=build description=<text> (manual)`. The build gate never executes `type=skipped`; it lists each one as an unchecked "Manual follow-up" in the PASSED comment. Never emit `target="none"` (or any placeholder target) with `command`/`behavior`: the gate would run it as a shell command, and the check is reported `malformed-check`. Include at least one machine-checkable check whenever one can be derived: a spec made only of manual checks passes with human review required (like the `ac-skip` sentinel), and a spec whose remaining checks are all malformed verifies nothing and blocks the build as a spec-defect.
|
|
730
|
+
|
|
731
|
+
**Emission-time dry-run of `command` targets** <!-- Added: forge#3825 -->: before posting, run each repo-local, read-only `command` target (`bash scripts/*.sh`, `node bin/*`) once from the base checkout with `timeout 20`. A target that exits 2 with `usage` in stderr is missing a required argument: fix it (add the argument) or reclassify it as manual. Flag only exit 2 plus a usage message; do not treat every non-zero exit as a defect, because a target that fails for a real reason (missing file, failing assertion) is a legitimate check that must still FAIL until the implementation lands. Skip any target that is not clearly read-only.
|
|
732
|
+
|
|
729
733
|
**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.
|
|
730
734
|
|
|
731
735
|
**Class-wide checks (review-finding issues)**: when a Pattern Sweep was recorded, emit at least one `ACCEPTANCE_CHECK` that verifies the fix covers every `fix` row (for example a `command` check that the defective pattern no longer matches anywhere in the swept paths) and one that verifies the added test covers a representative set of instances rather than the reviewer's single repro. <!-- Added: forge#3449 -->
|
|
@@ -129,7 +129,7 @@ Completed trails of other kinds are left in place (never deleted); only the inte
|
|
|
129
129
|
- If `REMEDIATION_COMPLETE_N` or `REMEDIATION_TRAIL_N` is empty (comments or `scripts/trusted-comments.sh` unreadable or unresolvable) → fail closed: EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "github-unavailable: remediation trail unreadable (comments or scripts/trusted-comments.sh unresolvable)". Never treat unreadable as "no trail" and never as `ALREADY_DONE`.
|
|
130
130
|
- If `REMEDIATION_COMPLETE_N` is at least 1 (a trusted comment that starts with `<!-- FORGE:REMEDIATION -->` and contains `<!-- FORGE:REMEDIATION:COMPLETE -->`) → EXIT `REMEDIATE_RESULT: status: ALREADY_DONE`. **Single-attempt semantics (AC5)**: once a genuine `FORGE:REMEDIATION:COMPLETE` trail comment 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. A comment that only quotes the marker mid-body (review findings, an `INPR_FIX` work order, a disposition record) or comes from an untrusted author never counts.
|
|
131
131
|
- **Merged-PR routing (forge#3685)**: if `PR_ALREADY_MERGED=true` and `REMEDIATION_COMPLETE_N` is 0 (the fail-closed unreadable-trail exit above already ran, so the count is known): if the linked issue is already `CLOSED` with the `workflow:merged` label (`ISSUE_STATE`), EXIT `REMEDIATE_RESULT: status: ALREADY_DONE` (landed and finished, nothing to do). Otherwise (not yet `workflow:merged`/CLOSED, no completed trail) set `RESUME_AT_M7=true` and `PR_ALREADY_MERGED=true`: skip Phases M1-M6 and continue at Phase M7, whose merged-first branch is exclusive, runs the idempotent `merged` label bookkeeping, sets `AUTO-LANDED` and goes to M8, which posts the trail and drives close. No park evidence is required. A `FORGE:REMEDIATION_HELD` record never counts toward `REMEDIATION_COMPLETE_N`.
|
|
132
|
-
- **Liveness step (forge#3792)**: when the PR is open, the issue carries `needs-human` or `workflow:remediating` and `REMEDIATION_COMPLETE_N` is 0, run this BEFORE any partial comment is deleted or anything is pushed. Partial trail comments are not evidence of a live run; the script decides from the `FORGE:STATE` lease and the routed re-review. Pass `{LEASE_HOLDER}` only if it matches `^[A-Za-z0-9._-]{1,128}$`, else an empty string.
|
|
132
|
+
- **Liveness step (forge#3792)**: when the PR is open, the issue carries `needs-human` or `workflow:remediating` and `REMEDIATION_COMPLETE_N` is 0, run this BEFORE any partial comment is deleted or anything is pushed. Partial trail comments are not evidence of a live run; the script decides from the `FORGE:STATE` lease and the routed re-review. A review that handed off to remediation (a trusted `FORGE:{CI|INPR|BASESYNC}_REMEDIATION: pr={N}` marker at or after the newest route, e.g. a base-sync handoff before any panel was dispatched) is never in flight: it never defers and spends none of the deferral budget. Pass `{LEASE_HOLDER}` only if it matches `^[A-Za-z0-9._-]{1,128}$`, else an empty string.
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
135
|
LV_SCRIPT=$(forge_resolve remediate-liveness.sh)
|
|
@@ -137,7 +137,7 @@ Completed trails of other kinds are left in place (never deleted); only the inte
|
|
|
137
137
|
echo "$LV_OUT"
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
`LV_RC` 0 (dead): continue with the cleanup below. 10 (`DEFER`, the script posted a `FORGE:REMEDIATION_DEFERRED` record): EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker `deferred-live-run: {detail}`, touching nothing (no deletes, labels or push; the plain `git push` is the atomic backstop, a non-fast-forward race is rejected and parks at M4). 11 (`EXHAUSTED`, 3 deferrals already): a normal `BLOCKED` exit with `needs-human`. Any other rc: EXIT `BLOCKED`, blocker `github-unavailable: liveness: {detail}`.
|
|
140
|
+
`LV_RC` 0 (dead): continue with the cleanup below. 10 (`DEFER`, the script posted a `FORGE:REMEDIATION_DEFERRED` record): EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker `deferred-live-run: {detail}`, touching nothing (no deletes, labels or push; the plain `git push` is the atomic backstop, a non-fast-forward race is rejected and parks at M4). 11 (`EXHAUSTED`, 3 deferrals already): a normal `BLOCKED` exit with `needs-human`. Any other rc: EXIT `BLOCKED`, blocker `github-unavailable: liveness: {detail}`. The engine posts a `<!-- FORGE:REMEDIATION_DISPATCH: pr={pr} -->` record on every dispatch to scope a deferral to its own dispatch; it is not a trail or completion marker and the partial-trail cleanup ignores it.
|
|
141
141
|
- If `REMEDIATION_COMPLETE_N` is 0 and `REMEDIATION_TRAIL_N` is at least 1 and the liveness step above said **dead** → 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 (ids from `PARTIAL_PR_COMMENT_IDS` above and the same anchored filter over the issue's comments), then continue below as a fresh attempt:
|
|
142
142
|
```bash
|
|
143
143
|
for _id in $PARTIAL_PR_COMMENT_IDS; do gh api repos/{GH_REPO}/issues/comments/$_id -X DELETE 2>/dev/null || true; done
|
|
@@ -185,7 +185,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
188
|
-
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
188
|
+
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> max_severity=<MEDIUM|HIGH|CRITICAL> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM/HIGH/CRITICAL findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
189
189
|
- **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.
|
|
190
190
|
|
|
191
191
|
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (add it and remove `workflow:remediating` if the issue entered as `workflow:remediating`), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
@@ -218,12 +218,28 @@ if [ -d "{WORKTREE_PATH}" ]; then
|
|
|
218
218
|
git -C "{WORKTREE_PATH}" fetch origin
|
|
219
219
|
git -C "{WORKTREE_PATH}" checkout {HEAD_BRANCH}
|
|
220
220
|
git -C "{WORKTREE_PATH}" reset --hard "origin/{HEAD_BRANCH}"
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
elif ! WT_ERR=$(git worktree add "{WORKTREE_PATH}" {HEAD_BRANCH} "origin/{HEAD_BRANCH}" 2>&1); then
|
|
222
|
+
# Branch already checked out elsewhere: reuse that worktree (never the main checkout).
|
|
223
|
+
EXISTING_WT=""; MAIN_WT=""; CUR_WT=""
|
|
224
|
+
while IFS= read -r line; do
|
|
225
|
+
case "$line" in
|
|
226
|
+
"worktree "*) CUR_WT="${line#worktree }"; [ -z "$MAIN_WT" ] && MAIN_WT="$CUR_WT" ;;
|
|
227
|
+
"branch refs/heads/{HEAD_BRANCH}") [ -z "$EXISTING_WT" ] && EXISTING_WT="$CUR_WT" ;;
|
|
228
|
+
esac
|
|
229
|
+
done < <(git worktree list --porcelain)
|
|
230
|
+
WORKTREE_PATH="" # stays empty (BLOCKED) unless the reuse is provably safe
|
|
231
|
+
if [ -n "$EXISTING_WT" ] && [ "$EXISTING_WT" != "$MAIN_WT" ] && [ -d "$EXISTING_WT" ] \
|
|
232
|
+
&& [ -z "$(git -C "$EXISTING_WT" status --porcelain)" ] \
|
|
233
|
+
&& [ "$(git -C "$EXISTING_WT" rev-list --count "origin/{HEAD_BRANCH}..HEAD")" = "0" ]; then
|
|
234
|
+
git -C "$EXISTING_WT" fetch origin && git -C "$EXISTING_WT" reset --hard "origin/{HEAD_BRANCH}" && WORKTREE_PATH="$EXISTING_WT"
|
|
235
|
+
else echo "$WT_ERR" >&2; fi
|
|
236
|
+
echo "WORKTREE_PATH=$WORKTREE_PATH" # use this printed value for every later {WORKTREE_PATH}
|
|
223
237
|
fi
|
|
224
238
|
```
|
|
225
239
|
|
|
226
|
-
|
|
240
|
+
**Main-checkout ban**: remediation never runs `checkout`, `switch`, `reset`, `stash`, or `merge` in the main checkout (`{REPO_PATH}`), not even as a fallback for a failed `git worktree add`; every mutating git command targets `{WORKTREE_PATH}` via `git -C`. A reused worktree is never reset when it has uncommitted changes or commits not on `origin/{HEAD_BRANCH}`, and an unresolvable one is not improvised around: `WORKTREE_PATH` stays empty and an empty value after this block means BLOCKED. <!-- Added: forge#3650 -->
|
|
241
|
+
|
|
242
|
+
If the worktree/branch checkout fails for any reason (branch deleted, force-pushed out from under us, checked out in the main checkout, etc.): post a comment, add `needs-human`, EXIT `REMEDIATE_RESULT: status: BLOCKED`.
|
|
227
243
|
|
|
228
244
|
---
|
|
229
245
|
|