forgedock 1.17.0 → 1.18.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 +24 -2
- package/bin/engine/phases.mjs +153 -102
- package/bin/engine/runlog.mjs +7 -0
- package/bin/engine-cli.mjs +5 -0
- package/bin/engine.mjs +84 -10
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +63 -4
- package/bin/stats/github-source.mjs +89 -18
- package/bin/stats/stats.test.mjs +89 -1
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +142 -519
- package/commands/review-pr.md +490 -3128
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +2 -1
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +107 -86
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +9 -12
- package/commands/work-on/remediate.md +27 -13
- package/commands/work-on/review.md +5 -3
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +44 -25
- package/docs/OUTCOME-CODES.md +7 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +2 -1
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +6 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +1 -1
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/close-parent-c3.test.sh +69 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/forge-root.test.sh +13 -11
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +48 -12
- package/scripts/review-prepare.test.sh +19 -1
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.test.sh +3 -3
package/commands/test-gate.md
CHANGED
|
@@ -439,23 +439,38 @@ BASELINE_RESULTS=""
|
|
|
439
439
|
|
|
440
440
|
# Run the same tests against the base branch state to establish baseline
|
|
441
441
|
# If base-branch tests also fail, those are pre-existing failures — not batch-introduced
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
# Use a detached worktree at the base branch — never stash/checkout in the main checkout
|
|
443
|
+
BASE_WT=$(mktemp -d)
|
|
444
|
+
git -C "$REPO_PATH" worktree add --detach "$BASE_WT" "origin/${BASE_BRANCH}" 2>/dev/null \
|
|
445
|
+
|| { echo "WARNING: could not create baseline worktree — baseline unavailable (failures will count as batch-introduced)"; rmdir "$BASE_WT" 2>/dev/null; BASE_WT=""; }
|
|
446
|
+
# A fresh worktree has no installed deps or local env files: link the main checkout's (read-only use), at the repo
|
|
447
|
+
# root and in every cluster working_dir, so a baseline cluster fails only for code reasons, never for a missing install.
|
|
448
|
+
if [ -n "$BASE_WT" ]; then
|
|
449
|
+
{ echo .; yq '.verification.integration_tests[].working_dir // "."' "$CONFIG_FILE" 2>/dev/null; } | sort -u | while IFS= read -r _wd; do
|
|
450
|
+
for _dep in node_modules .venv venv .env .env.local .env.test; do
|
|
451
|
+
[ -e "${REPO_PATH}/${_wd}/${_dep}" ] && [ ! -e "${BASE_WT}/${_wd}/${_dep}" ] && [ -d "${BASE_WT}/${_wd}" ] \
|
|
452
|
+
&& ln -s "${REPO_PATH}/${_wd}/${_dep}" "${BASE_WT}/${_wd}/${_dep}"
|
|
453
|
+
done
|
|
454
|
+
done # one working_dir per line: a path with spaces is never word-split
|
|
455
|
+
fi
|
|
444
456
|
|
|
445
457
|
for i in $(seq 0 $((CLUSTER_COUNT - 1))); do
|
|
446
458
|
CLUSTER=$(yq ".verification.integration_tests[${i}].cluster" "$CONFIG_FILE")
|
|
447
459
|
CMD=$(yq ".verification.integration_tests[${i}].command" "$CONFIG_FILE")
|
|
448
460
|
WORKING_DIR=$(yq ".verification.integration_tests[${i}].working_dir // \".\"" "$CONFIG_FILE")
|
|
449
461
|
|
|
450
|
-
if
|
|
462
|
+
if [ -z "$BASE_WT" ]; then
|
|
463
|
+
# No baseline: never claim pre-existing. A bundle failure in this cluster counts as batch-introduced (fail closed).
|
|
464
|
+
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: UNAVAILABLE"
|
|
465
|
+
elif (cd "${BASE_WT}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
|
|
451
466
|
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: PASS"
|
|
452
467
|
else
|
|
453
468
|
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: FAIL (pre-existing)"
|
|
454
469
|
fi
|
|
455
470
|
done
|
|
456
471
|
|
|
457
|
-
#
|
|
458
|
-
git
|
|
472
|
+
# Remove the baseline worktree (the main checkout was never touched)
|
|
473
|
+
[ -n "$BASE_WT" ] && git -C "$REPO_PATH" worktree remove --force "$BASE_WT" 2>/dev/null || true
|
|
459
474
|
|
|
460
475
|
echo "Baseline results:"
|
|
461
476
|
echo -e "$BASELINE_RESULTS"
|
|
@@ -475,10 +490,10 @@ while IFS= read -r result_line; do
|
|
|
475
490
|
if echo "$STATUS" | grep -q "^FAIL"; then
|
|
476
491
|
# Check if this cluster also failed in baseline
|
|
477
492
|
BASELINE_STATUS=$(echo -e "$BASELINE_RESULTS" | grep "^- ${CLUSTER}:" | grep -oP ':\s+\K.+' || echo "PASS")
|
|
478
|
-
if echo "$BASELINE_STATUS" | grep -q "^
|
|
479
|
-
#
|
|
493
|
+
if ! echo "$BASELINE_STATUS" | grep -q "^FAIL"; then
|
|
494
|
+
# Baseline PASS (or UNAVAILABLE: no proof it was pre-existing) but fails in the bundle — batch-introduced failure
|
|
480
495
|
BATCH_FAILURES="${BATCH_FAILURES}\n${CLUSTER}"
|
|
481
|
-
echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline:
|
|
496
|
+
echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline: ${BASELINE_STATUS} → bundle: FAIL)"
|
|
482
497
|
else
|
|
483
498
|
echo "PRE-EXISTING FAILURE: cluster '${CLUSTER}' (baseline: FAIL → bundle: FAIL — not this batch)"
|
|
484
499
|
fi
|
|
@@ -534,7 +549,7 @@ else
|
|
|
534
549
|
cat <<ISSUE_EOF > "$TESTGATE_FAIL_BODY_FILE"
|
|
535
550
|
## Problem
|
|
536
551
|
|
|
537
|
-
The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle.
|
|
552
|
+
The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. It was not observed failing on \`${BASE_BRANCH}\` (baseline: PASS, or UNAVAILABLE when the baseline could not run — then treated as batch-introduced, fail closed).
|
|
538
553
|
|
|
539
554
|
## Root Cause (if known)
|
|
540
555
|
|
|
@@ -574,7 +589,7 @@ $(echo -e "$SOURCE_ISSUES_LIST")
|
|
|
574
589
|
\`\`\`
|
|
575
590
|
3. Observe failure. Compare against \`${BASE_BRANCH}\` baseline:
|
|
576
591
|
\`\`\`bash
|
|
577
|
-
git
|
|
592
|
+
git worktree add --detach "$BASE_WT" origin/${BASE_BRANCH} # run the command inside $BASE_WT
|
|
578
593
|
$(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
|
|
579
594
|
\`\`\`
|
|
580
595
|
ISSUE_EOF
|
|
@@ -719,6 +719,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
|
719
719
|
|---|------|----------------|-----------------|-----|
|
|
720
720
|
| 1 | {FILE} | {FUNCTION} | {CHANGE} | {REASON} |
|
|
721
721
|
|
|
722
|
+
### Scope Baseline
|
|
723
|
+
<!-- Machine-readable scope baseline read by build B5.5's post-build scope check (forge#3862). Every
|
|
724
|
+
path in ### Affected Paths (including tests, docs and config the plan changes), one per line as
|
|
725
|
+
- path (repo-relative; a trailing slash means every file under that directory). The keys sit alone
|
|
726
|
+
on their lines. ESTIMATED_DIFF_LINES is the plan's own estimate (code plus tests, a whole number). -->
|
|
727
|
+
PLANNED_FILES:
|
|
728
|
+
- {FILE}
|
|
729
|
+
ESTIMATED_DIFF_LINES: {N}
|
|
730
|
+
|
|
722
731
|
### Pattern Sweep
|
|
723
732
|
<!-- Pass-through from the FORGE:INVESTIGATOR '### Pattern Sweep' table (Phase A2.7).
|
|
724
733
|
Omit this section when the investigation report had none. -->
|
|
@@ -799,13 +808,16 @@ SKIP_BODY=$(cat <<SKIP_EOF
|
|
|
799
808
|
|
|
800
809
|
**Skipped**: {SKIP_REASON}. No cross-path consistency risk — the builder proceeds with the investigation report + context briefing.
|
|
801
810
|
|
|
811
|
+
### Scope Baseline
|
|
812
|
+
PLANNED_FILES:
|
|
813
|
+
|
|
802
814
|
<!-- FORGE:ARCHITECT:COMPLETE -->
|
|
803
815
|
SKIP_EOF
|
|
804
816
|
)
|
|
805
817
|
gh issue comment {NUMBER} {GH_FLAG} --body "$SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
806
818
|
```
|
|
807
819
|
|
|
808
|
-
The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
|
|
820
|
+
The empty `PLANNED_FILES:` list means "no plan baseline": build B5.5 then skips the out-of-plan check (proceed, never block) and falls back to the investigation's `ESTIMATED_DIFF_LINES`. The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
|
|
809
821
|
|
|
810
822
|
---
|
|
811
823
|
|
|
@@ -165,6 +165,7 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
|
|
|
165
165
|
- For each acceptance criterion in the contract: implement it, then mentally verify it's met
|
|
166
166
|
- Do NOT add unrequested scope — contract out-of-scope items stay out of scope
|
|
167
167
|
- **Library callback verification**: When writing a lambda or callable that will be passed to a library/framework parameter (e.g., `prepared_statement_name_func=lambda: ""`, `key=lambda x: ...`), you MUST verify the expected calling convention BEFORE writing it. Check the library's default value for that parameter, its documentation, or its source code. A lambda with wrong arity causes `TypeError` at runtime — this is invisible to static analysis and linting. The P0 incident from PR #14391 was caused by `lambda _: ""` (1 arg) passed where SQLAlchemy expects 0 args.
|
|
168
|
+
- **Scratch placement (venvs, caches)**: create virtualenvs, package caches and other large scratch data under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree. Never use a literal `/tmp/<name>` path: a size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when the run finishes. <!-- Added: forge#3572 -->
|
|
168
169
|
- **Worktree-aware path derivation**: When writing shell code that derives repository paths, ALWAYS use `git rev-parse --show-toplevel` for the repo root in regular checkouts, or `git rev-parse --git-common-dir` (then `dirname`) to get the shared `.git` directory when the context may be a linked worktree. For worktree cleanup code specifically, ALWAYS use `--git-common-dir` — `--show-toplevel` returns the worktree path itself, NOT the main repo root. NEVER use `pwd`, relative paths, or `dirname` chains on `--show-toplevel` output for repo root derivation. Test path logic for both regular checkouts and linked worktrees. (Ref: review-findings #104, #105 — 4 PATH_DERIVATION defects, 15% of all review findings)
|
|
169
170
|
- **State machine completeness verification**: When implementing routing logic, state transition tables, or phase dispatch code (e.g., adding a `Skill()` call in a routing loop, adding a new phase to a state machine, creating a new subcommand file), you MUST run three checks BEFORE staging: (1) **Routing target existence** — for each `Skill("subcommand", ...)` call or file reference in routing logic, verify the target file exists at `commands/{subcommand-path}.md`; (2) **State reachability** — for each declared state or phase, verify at least one prior state or entry condition in the router routes to it; (3) **Subcommand invocation wiring** — for each `commands/work-on/*.md` file that declares its invocation condition (e.g., `Invoked by work-on.md routing loop, when X`), verify that condition is actually handled in the router. A missing target file, an unreachable state, or a declared-but-unwired subcommand will not surface until review. (Ref: review-findings #85, #116, #137 — 4 ROUTING defects, 15% of all review findings)
|
|
170
171
|
- **Migration safety checklist** *(trigger: diff includes `*.sql` files or files under a `migrations/` path)*: When any migration file is in the diff, verify ALL of the following BEFORE staging. Fix each violation inline — do not defer to review or the quality gate: (a) **Rollback file**: a corresponding down/rollback migration file exists (e.g. `0042_down_*.sql`, `rollback_*.sql`) or the migration is explicitly self-reversing (DROP of a previously-added column); (b) **NOT NULL safety**: any `ADD COLUMN ... NOT NULL` either includes a `DEFAULT` clause or is preceded by a backfill step — a NOT NULL column without DEFAULT locks the table and fails on existing rows; (c) **Constraint name consistency**: constraint names in the migration match ORM model declarations — a mismatch causes `alembic stamp` and FK introspection to silently diverge; (d) **CREATE TRIGGER idempotency**: every `CREATE TRIGGER` uses `CREATE OR REPLACE TRIGGER` or is preceded by `DROP TRIGGER IF EXISTS` — a bare `CREATE TRIGGER` fails on re-run in test and CI environments; (e) **Migration prefix uniqueness**: confirm the new file's numeric prefix does not already exist in the `infra/migrations/` tree (`ls infra/migrations/*.sql | xargs -n1 basename | grep -oE '^[0-9]+' | sort | uniq -d` prints duplicates) — a duplicate prefix hard-fails the deploy gate regardless of file content. <!-- Added: forge#373 -->
|
|
@@ -305,7 +306,7 @@ git add {CHANGED_FILES}
|
|
|
305
306
|
|
|
306
307
|
Do NOT run `git commit` here. The commit (with conventional commit message and issue reference) is made by validate.md Phase V5 after `GATE_PASSED=true`.
|
|
307
308
|
|
|
308
|
-
**Informational diff size** <!-- Added: forge#3450 -->: after staging, read the changed-line count so the builder comment and result carry it. This is a hint only. Implement never gates or decides: the
|
|
309
|
+
**Informational diff size** <!-- Added: forge#3450 -->: after staging, read the changed-line count so the builder comment and result carry it. This is a hint only. Implement never gates or decides: the scope check lives in build B5.5, which re-measures authoritatively against the plan (it needs only the `diff_lines=` key here). Leave `DIFF_LINES` empty when the script is unavailable or fails.
|
|
309
310
|
|
|
310
311
|
```bash
|
|
311
312
|
# <Script resolution block from work-on/build.md, verbatim>
|
|
@@ -153,6 +153,8 @@ gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-com
|
|
|
153
153
|
|
|
154
154
|
Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`. `forge.yaml` is usually gitignored, so it is absent from the worktree: every block below resolves `FORGE_CFG` to the worktree copy if present, else the main checkout's `forge.yaml` (via `git rev-parse --git-common-dir`). Before this, every verification command, learned test command and the SOPS chain check were silently skipped in worktree builds; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing.
|
|
155
155
|
|
|
156
|
+
**Scratch placement (venvs, caches)** <!-- Added: forge#3572 -->: any virtualenv, package cache or other large scratch data a verification command needs goes under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree, never a literal `/tmp/<name>` path. A size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when validation finishes.
|
|
157
|
+
|
|
156
158
|
**Track skipped checks** — initialize before any check runs:
|
|
157
159
|
```bash
|
|
158
160
|
SKIPPED_CHECKS=""
|
|
@@ -23,7 +23,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
23
23
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
|
|
24
24
|
**NEVER use plan mode (EnterPlanMode).**
|
|
25
25
|
|
|
26
|
-
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the one exception is the B0 gate-resume branch, which enters at B6.5 with its state verified). Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
27
27
|
|
|
28
28
|
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
29
29
|
|
|
@@ -108,7 +108,7 @@ Every exit path of this skill — COMPLETE, ALREADY_DONE, INVESTIGATION_COMPLETE
|
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
BUILD_RESULT:
|
|
111
|
-
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE |
|
|
111
|
+
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
112
112
|
branch: {BRANCH}
|
|
113
113
|
worktree: {WORKTREE_PATH}
|
|
114
114
|
blocker: {description if status=BLOCKED, else empty}
|
|
@@ -176,25 +176,10 @@ EXISTING_FAST_PATH=$(printf '%s' "$CYCLE_SLICE" | bash "$TRUSTED_SCRIPT" bodies
|
|
|
176
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
177
|
|
|
178
178
|
**Resume check**:
|
|
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
|
|
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 FAST_PATH read above (script resolution block verbatim; pipe `cycle_slice` into `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
180
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".
|
|
181
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).
|
|
182
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`.
|
|
183
|
-
- **Size-gate resume** (checked BEFORE the partial-BUILDER rule below; first match wins) <!-- Added: forge#3463 -->: if a `<!-- FORGE:BUILDER -->` comment exists without `<!-- FORGE:BUILDER:COMPLETE -->` AND the latest trusted `FORGE:DIFF_SIZE` comment has `result: OVER` or `result: COHESIVE`, the build stopped at a deliberate B5.5 exit (`NEEDS_DECOMPOSE`, the decompose-loop-guard Blocked exit, or a crash right after the `COHESIVE` comment was posted), not in an implement crash. Keep the BUILDER comment, do not re-run B2-B5, and re-enter at **Phase B5.5**. Read the marker through the same anchored, trust-filtered lookup B5.5 uses (script resolution block verbatim):
|
|
184
|
-
```bash
|
|
185
|
-
# <Script resolution block from above goes here, verbatim>
|
|
186
|
-
set -o pipefail
|
|
187
|
-
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
188
|
-
[ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ] || { echo "DIFF_SIZE_LOOKUP=FAILED (trusted-comments.sh unresolved)"; exit 0; }
|
|
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') \
|
|
191
|
-
|| { echo "DIFF_SIZE_LOOKUP=FAILED"; exit 0; }
|
|
192
|
-
LAST_RESULT=$(printf '%s\n' "$DS" | jq -r 'select(type == "string")' | sed -n 's/^result: *\([A-Z]*\).*/\1/p' | tail -n 1)
|
|
193
|
-
echo "DIFF_SIZE_RESULT=${LAST_RESULT:-<none>}"
|
|
194
|
-
```
|
|
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".
|
|
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).
|
|
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.
|
|
198
183
|
- 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
184
|
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 -->
|
|
200
185
|
```bash
|
|
@@ -391,7 +376,7 @@ Every item in the contract's `### Out of Scope` section must carry exactly one d
|
|
|
391
376
|
- **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.
|
|
392
377
|
- Free-text exclusions without a disposition are invalid. `->` is accepted for `→`.
|
|
393
378
|
|
|
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
|
|
379
|
+
**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 scope budget (a small fraction of the plan's own estimate: at most 20% of the current `ESTIMATED_DIFF_LINES`, or 200 lines when no estimate exists) 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
380
|
|
|
396
381
|
**File the `deferred` follow-ups** (skip the filing on resume when the contract already exists):
|
|
397
382
|
|
|
@@ -593,50 +578,91 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
593
578
|
|
|
594
579
|
**After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
|
|
595
580
|
- `status: COMPLETE` → continue to B5.5
|
|
596
|
-
- `status: ALREADY_DONE` → continue to B5.5 (
|
|
581
|
+
- `status: ALREADY_DONE` → continue to B5.5 (scope-check what's already there)
|
|
597
582
|
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; verify a `<!-- FORGE:INVESTIGATION:DELIVERABLES -->` comment exists on the issue (the implement child posts it and removes `workflow:building`; if it is missing, post it listing the created issues and remove the label), then print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5). The engine keys off that marker, not the result text. <!-- Added: forge#3534 -->
|
|
598
583
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
599
584
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
600
585
|
- Returned running/backgrounded/empty (no `IMPLEMENT_RESULT:`) → do not end the turn to wait; re-read the `FORGE:BUILDER` comment and the worktree state; if `FORGE:BUILDER:COMPLETE` is present consume it, and if the worktree shows in-flight edits or a fresh commit treat the child as possibly live; before re-invoking, apply the bounded re-invoke rule (at most 2, then `child-stalled: implement`)
|
|
601
|
-
# MUST CONTINUE to Phase B5.5 — implement result is intermediate, NOT terminal (the
|
|
586
|
+
# MUST CONTINUE to Phase B5.5 — implement result is intermediate, NOT terminal (the scope check and validation still follow).
|
|
602
587
|
|
|
603
588
|
---
|
|
604
589
|
|
|
605
|
-
## Phase B5.5:
|
|
590
|
+
## Phase B5.5: Scope Check
|
|
606
591
|
|
|
607
|
-
|
|
592
|
+
A finished build is never discarded and never routed to decompose: decomposition is a planning decision (investigate / architect), not a post-build one. This phase compares the staged diff with the plan, measures a deterministic `review_weight`, and records a scope outcome. Size never blocks; it scales review depth. It measures the staged diff (implement staged everything in I4; nothing is committed until validate V5) and edits code only for the opt-in out-of-plan trim in Step 4. <!-- Added: forge#3862 -->
|
|
608
593
|
|
|
609
|
-
**Skip** (continue to B6) when the task type is Investigation (B5 already exited)
|
|
594
|
+
**Skip** (continue to B6) only when the task type is Investigation (B5 already exited). Never skip because a lookup failed: unknown is not in plan.
|
|
610
595
|
|
|
611
|
-
**Step 1 —
|
|
596
|
+
**Step 1 — baseline.** The plan is the scope baseline: the latest trusted `FORGE:ARCHITECT` comment of the current-cycle slice carries `PLANNED_FILES:` (a `- path` list; a trailing `/` is a directory prefix) and `ESTIMATED_DIFF_LINES: N` under `### Scope Baseline`. An architect skip marker has an empty list, which means "no plan baseline": proceed with no out-of-plan check. The estimate falls back to the issue body's own-line `ESTIMATED_DIFF_LINES: N` (written by `work-on:decompose` D3), then to the latest trusted `FORGE:INVESTIGATOR` comment's. Paste the Script resolution block and the `cycle_slice` function from B0 first (shell state does not persist); quote every interpolation:
|
|
612
597
|
|
|
613
598
|
```bash
|
|
614
599
|
# <Script resolution block from above goes here, verbatim>
|
|
600
|
+
# <cycle_slice function from B0, verbatim>
|
|
601
|
+
set -o pipefail
|
|
602
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
603
|
+
[ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ] || { echo "BASELINE_LOOKUP=FAILED (trusted-comments.sh unresolved)"; exit 0; }
|
|
604
|
+
SLICE=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | cycle_slice) && [ -n "$SLICE" ] || { echo "BASELINE_LOOKUP=FAILED"; exit 0; }
|
|
605
|
+
ARCH_BODY=$(printf '%s' "$SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:ARCHITECT' | jq -rs 'map(select(type == "string")) | last // ""') || { echo "BASELINE_LOOKUP=FAILED"; exit 0; }
|
|
606
|
+
INV_BODY=$(printf '%s' "$SLICE" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:INVESTIGATOR' | jq -rs 'map(select(type == "string")) | last // ""') || { echo "BASELINE_LOOKUP=FAILED"; exit 0; }
|
|
607
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq .body) || { echo "BASELINE_LOOKUP=FAILED"; exit 0; }
|
|
608
|
+
SCOPE_DIR="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$SCOPE_DIR"
|
|
609
|
+
PLAN_FILE="$SCOPE_DIR/scope-plan-{NUMBER}" # fixed path: Step 2 runs in a fresh shell and re-derives it
|
|
610
|
+
printf '%s\n' "$ARCH_BODY" | awk '/^PLANNED_FILES:[ \t]*$/{f=1;next} f&&/^[ \t]*[-*][ \t]+/{print;next} f{exit}' > "$PLAN_FILE"
|
|
611
|
+
EST_RE='s/^ESTIMATED_DIFF_LINES: *\([0-9][0-9]*\) *$/\1/p'
|
|
612
|
+
ESTIMATE=$(printf '%s\n' "$ARCH_BODY" | sed -n "$EST_RE" | tail -n 1)
|
|
613
|
+
[ -n "$ESTIMATE" ] || ESTIMATE=$(printf '%s\n' "$ISSUE_BODY" | sed -n "$EST_RE" | tail -n 1)
|
|
614
|
+
[ -n "$ESTIMATE" ] || ESTIMATE=$(printf '%s\n' "$INV_BODY" | sed -n "$EST_RE" | tail -n 1)
|
|
615
|
+
printf '%s' "${ESTIMATE:-}" > "$SCOPE_DIR/scope-estimate-{NUMBER}"
|
|
616
|
+
echo "BASELINE_LOOKUP=ok PLAN_FILE=${PLAN_FILE} ESTIMATE=${ESTIMATE:-none}"
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
`BASELINE_LOOKUP=FAILED` → Blocked exit with blocker `github-unavailable: could not read the scope baseline (FORGE:ARCHITECT)` (transient; the router retry rule applies; no `needs-human`). Never read a failed lookup as "no plan" or "in plan". A read that succeeds with no `PLANNED_FILES:` entries is a legitimate "no plan baseline".
|
|
620
|
+
|
|
621
|
+
**Step 2 — measure.** Config (all values are config, none is a magic number; defaults are generous and are calibrated from telemetry, forge#3834): `build.scope.test_weight` (percent weight of test lines in `review_weight`, default 50), `build.scope.module_depth` (path segments that define a module, default 2), `build.scope.overrun_pct` (actual whitespace-insensitive lines over the plan estimate, in percent, that count as scope creep, default 200), `build.scope.exclude_globs`, `review.depth.extended_weight` (`review_weight` at or above which review is extended, default 1500) and `review.depth.extended_modules` (modules touched at or above which review is extended, default 6). The deprecated `build.diff_size.exclude_globs` is read as an alias of `build.scope.exclude_globs` and a positive `build.diff_size.threshold` as an alias of `review.depth.extended_weight` for one release; when an alias supplied the value, add `deprecated_config: build.diff_size.*` to the comment (Step 5). The threshold no longer blocks anything.
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# <Script resolution block from above goes here, verbatim>
|
|
625
|
+
# Step 1's variables do not survive into this fence: reload the baseline it persisted.
|
|
626
|
+
SCOPE_DIR="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
627
|
+
PLAN_FILE="$SCOPE_DIR/scope-plan-{NUMBER}"; [ -f "$PLAN_FILE" ] || : > "$PLAN_FILE"
|
|
628
|
+
ESTIMATE=$(cat "$SCOPE_DIR/scope-estimate-{NUMBER}" 2>/dev/null || true)
|
|
615
629
|
# Fresh origin/base for the merge-base measurement: retry with 10s/30s/60s backoff, record whether it ever succeeded
|
|
616
630
|
FETCH_OK=0
|
|
617
631
|
for _delay in 0 10 30 60; do
|
|
618
632
|
[ "$_delay" -gt 0 ] && sleep "$_delay"
|
|
619
633
|
if git -C "{WORKTREE_PATH}" fetch origin "{PR_BASE}" 2>/dev/null; then FETCH_OK=1; break; fi
|
|
620
634
|
done
|
|
621
|
-
|
|
635
|
+
TEST_WEIGHT=$(yq '.build.scope.test_weight // 50' forge.yaml 2>/dev/null || echo 50)
|
|
636
|
+
MODULE_DEPTH=$(yq '.build.scope.module_depth // 2' forge.yaml 2>/dev/null || echo 2)
|
|
637
|
+
OVERRUN_PCT=$(yq '.build.scope.overrun_pct // 200' forge.yaml 2>/dev/null || echo 200)
|
|
638
|
+
EXT_WEIGHT=$(yq '.review.depth.extended_weight // ""' forge.yaml 2>/dev/null || true)
|
|
639
|
+
EXT_MODULES=$(yq '.review.depth.extended_modules // 6' forge.yaml 2>/dev/null || echo 6)
|
|
640
|
+
DEPRECATED=""
|
|
641
|
+
if [ -z "$EXT_WEIGHT" ] || [ "$EXT_WEIGHT" = "null" ]; then
|
|
642
|
+
OLD_THRESHOLD=$(yq '.build.diff_size.threshold // 0' forge.yaml 2>/dev/null || echo 0)
|
|
643
|
+
case "$OLD_THRESHOLD" in ''|*[!0-9]*|0) EXT_WEIGHT=1500 ;; *) EXT_WEIGHT="$OLD_THRESHOLD"; DEPRECATED="build.diff_size.threshold" ;; esac
|
|
644
|
+
fi
|
|
622
645
|
EXCLUDES=()
|
|
623
646
|
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
624
|
-
< <(yq '.build.diff_size.exclude_globs // [] | .[]' forge.yaml 2>/dev/null || true)
|
|
647
|
+
< <(yq '(.build.scope.exclude_globs // []) + (.build.diff_size.exclude_globs // []) | .[]' forge.yaml 2>/dev/null || true)
|
|
625
648
|
SCRIPT_REF=$(resolve_script 'diff-size')
|
|
626
649
|
case "$SCRIPT_REF" in
|
|
627
|
-
prose:*) echo "
|
|
650
|
+
prose:*) echo "SCOPE_CHECK: scripts/diff-size.sh unresolved (prose tier) — check skipped with a note in the FORGE:BUILDER comment" ;;
|
|
628
651
|
*)
|
|
629
|
-
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --
|
|
652
|
+
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --test-weight "$TEST_WEIGHT" --module-depth "$MODULE_DEPTH" --overrun-pct "$OVERRUN_PCT" \
|
|
653
|
+
--plan-files "$PLAN_FILE" ${ESTIMATE:+--estimate "$ESTIMATE"} ${EXCLUDES[@]+"${EXCLUDES[@]}"}) && SIZE_RC=0 || SIZE_RC=$?
|
|
630
654
|
echo "SIZE_RC=${SIZE_RC}"; echo "FETCH_OK=${FETCH_OK}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
631
655
|
esac
|
|
632
656
|
```
|
|
633
657
|
|
|
634
|
-
- Prose tier (script unresolvable) → skip the
|
|
635
|
-
- Exit 2, empty output, or output without `diff_lines=`, `
|
|
636
|
-
- The same unmeasurable result with `FETCH_OK=1`
|
|
637
|
-
- Otherwise parse `diff_lines`, `excluded_lines`, `
|
|
658
|
+
- Prose tier (script unresolvable) → skip the check, continue to B6 (the script is part of the install; a missing install must not strand every build).
|
|
659
|
+
- Exit 2, empty output, or output without `diff_lines=`, `review_weight=`, `modules=` and `scope=`, AND `FETCH_OK=0` (the base fetch failed after the 10s/30s/60s retries) → Blocked exit with blocker `github-unavailable: could not fetch origin/{PR_BASE} for scope measurement` (transient; the router retry rule applies).
|
|
660
|
+
- The same unmeasurable result with `FETCH_OK=1` → Blocked exit with blocker `scope-check-unavailable: diff-size.sh failed (rc=<N>)`. Either way, do NOT treat an unmeasurable diff as in plan.
|
|
661
|
+
- Otherwise parse `diff_lines`, `excluded_lines`, `ws_lines`, `review_weight`, `modules`, `scope` (`ok` | `out_of_plan` | `no_baseline`), `plan_files`, `out_of_plan_count`, `estimate`, `overrun` and the `out_of_plan=` and `top=` lines.
|
|
638
662
|
|
|
639
|
-
**Step
|
|
663
|
+
**Step 3 — classify.** `review_depth` is `EXTENDED` when `review_weight` >= `EXT_WEIGHT` or `modules` >= `EXT_MODULES`, else `STANDARD`. The scope is flagged when `scope=out_of_plan` or `overrun=true` (an unparseable `scope=`/`overrun=` reads as flagged, never as clean). A flagged scope also sets `review_depth: EXTENDED`.
|
|
664
|
+
|
|
665
|
+
**Step 4 — look up an override and decide** (override lookup only when the scope is flagged). The lookup is anchored, trust-filtered through `scripts/trusted-comments.sh` (trusted association, Bot, or `FORGE_TRAIL_TRUSTED_LOGINS`), and fails closed:
|
|
640
666
|
|
|
641
667
|
```bash
|
|
642
668
|
# <Script resolution block from above goes here, verbatim>
|
|
@@ -650,71 +676,67 @@ JUSTIFICATION=$(printf '%s\n' "$OVR" | jq -r 'select(type == "string") | split("
|
|
|
650
676
|
if [ -n "$JUSTIFICATION" ]; then echo "OVERRIDE=VALID"; else echo "OVERRIDE=NONE"; fi
|
|
651
677
|
```
|
|
652
678
|
|
|
653
|
-
- `OVERRIDE_LOOKUP=FAILED` → Blocked exit with blocker `
|
|
654
|
-
-
|
|
679
|
+
- `OVERRIDE_LOOKUP=FAILED` → Blocked exit with blocker `scope-check-unavailable: could not read FORGE:SIZE_OVERRIDE` (a transient GitHub failure uses the `github-unavailable:` prefix per the retry rule above). Never read a failed lookup as "no override".
|
|
680
|
+
- A marker with an empty justification is not valid.
|
|
681
|
+
|
|
682
|
+
| Condition | Result | Action |
|
|
683
|
+
|-----------|--------|--------|
|
|
684
|
+
| scope not flagged, `review_depth=STANDARD` | `OK` | Post/refresh `FORGE:DIFF_SIZE`, continue to B6 |
|
|
685
|
+
| scope not flagged, `review_depth=EXTENDED` | `EXTENDED_REVIEW` | Post/refresh `FORGE:DIFF_SIZE` (review extends: more reviewers or per-file slices, read by review-pr), continue to B6. Never a block, never a split |
|
|
686
|
+
| scope flagged, valid `FORGE:SIZE_OVERRIDE` | `OVERRIDDEN` | Post/refresh `FORGE:DIFF_SIZE` quoting the justification, `review_depth` as classified (a valid override suppresses only the scope flag, not extended review), continue to B6 |
|
|
687
|
+
| scope flagged, no valid override | `SCOPE_FLAGGED` | Flag: list every `out_of_plan=` path and the overrun in the comment so review checks them (a scope-creep note, not a block), `review_depth: EXTENDED`, continue to B6. When `build.scope.trim_out_of_plan` is `true`, also trim (below) |
|
|
688
|
+
|
|
689
|
+
**Trim out-of-plan scope** (opt-in: `build.scope.trim_out_of_plan: true`, default `false`; only when `out_of_plan_count` is at most `build.scope.trim_max_files`, default 3; never for `overrun=true` alone). Each out-of-plan path's hunks move out of this PR into the batched follow-up checklist (forge#3843) instead of being discarded, so no work is lost:
|
|
690
|
+
|
|
691
|
+
```bash
|
|
692
|
+
# <Script resolution block from above goes here, verbatim>
|
|
693
|
+
MB=$(git -C "{WORKTREE_PATH}" merge-base HEAD "origin/{PR_BASE}") || { echo "TRIM=FAILED"; exit 0; }
|
|
694
|
+
PATCH_PATH="$(git -C "{WORKTREE_PATH}" rev-parse --git-common-dir)/forge-trimmed-{NUMBER}.patch"
|
|
695
|
+
DEFERRED_TSV=""
|
|
696
|
+
# OOP_PATHS: the out_of_plan= values, one per line (data, never evaluated)
|
|
697
|
+
while IFS= read -r _p; do
|
|
698
|
+
[ -n "$_p" ] || continue
|
|
699
|
+
git -C "{WORKTREE_PATH}" diff --cached --binary "$MB" -- "$_p" >> "$PATCH_PATH" || { echo "TRIM=FAILED"; exit 0; }
|
|
700
|
+
if git -C "{WORKTREE_PATH}" cat-file -e "${MB}:${_p}" 2>/dev/null; then
|
|
701
|
+
git -C "{WORKTREE_PATH}" restore --source="$MB" --staged --worktree -- "$_p" || { echo "TRIM=FAILED"; exit 0; }
|
|
702
|
+
else
|
|
703
|
+
git -C "{WORKTREE_PATH}" rm -q -f --cached -- "$_p" && rm -f -- "{WORKTREE_PATH}/$_p" || { echo "TRIM=FAILED"; exit 0; }
|
|
704
|
+
fi
|
|
705
|
+
DEFERRED_TSV="${DEFERRED_TSV}${_p}"$'\t'"out-of-plan hunks trimmed from #{NUMBER}; saved in ${PATCH_PATH}"$'\t'$'\n'
|
|
706
|
+
done <<< "$OOP_PATHS"
|
|
707
|
+
printf '%s' "$DEFERRED_TSV" | bash "$(resolve_script 'file-deferred-followups' | sed 's/^[^:]*://')" --parent {NUMBER} --repo {GH_REPO}; echo "DEFERRED_RC=$?"
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`TRIM=FAILED` or a non-zero `DEFERRED_RC` → do not trim further; keep whatever state is staged, fall back to the flag outcome (`SCOPE_FLAGGED`, nothing lost), and say so in the comment. After a successful trim re-run Step 2 once so `FORGE:DIFF_SIZE` records the trimmed numbers, set `trimmed: <N> file(s) -> #<follow-up>` and result `SCOPE_FLAGGED`.
|
|
655
711
|
|
|
656
|
-
**Step
|
|
712
|
+
**Step 5 — record.** Set `DIFF_LINES`, `EXCLUDED_LINES`, `WS_LINES`, `REVIEW_WEIGHT`, `MODULES`, `ESTIMATE`, `SCOPE`, `REVIEW_DEPTH` and `RESULT` 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). `diff_lines` stays the first field close.md and the protocol read:
|
|
657
713
|
|
|
658
714
|
```bash
|
|
659
715
|
# <Script resolution block from above goes here, verbatim>
|
|
660
716
|
BODY="<!-- FORGE:DIFF_SIZE -->
|
|
661
|
-
## Diff Size
|
|
717
|
+
## Diff Size and Scope
|
|
662
718
|
|
|
663
719
|
diff_lines: ${DIFF_LINES}
|
|
664
720
|
excluded_lines: ${EXCLUDED_LINES}
|
|
665
|
-
|
|
721
|
+
ws_lines: ${WS_LINES}
|
|
722
|
+
review_weight: ${REVIEW_WEIGHT}
|
|
723
|
+
modules: ${MODULES}
|
|
724
|
+
estimated_diff_lines: ${ESTIMATE:-none}
|
|
725
|
+
scope: ${SCOPE}
|
|
726
|
+
review_depth: ${REVIEW_DEPTH}
|
|
666
727
|
result: ${RESULT}
|
|
667
|
-
${
|
|
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"
|
|
728
|
+
${DEPRECATION_NOTE:-}${SCOPE_NOTE:-}"
|
|
729
|
+
_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" estimate="${ESTIMATE:-0}" actual="$WS_LINES" review_weight="$REVIEW_WEIGHT" modules="$MODULES" scope="$SCOPE" review_depth="$REVIEW_DEPTH" result="$RESULT" >/dev/null 2>&1 || true # forge#3834 telemetry (final result: estimate vs actual, review_weight, scope outcome, depth)
|
|
669
730
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
670
731
|
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)
|
|
671
732
|
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"
|
|
672
733
|
else run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"; fi
|
|
673
734
|
```
|
|
674
735
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
**Step 4 — decide.**
|
|
678
|
-
|
|
679
|
-
| Condition | Result | Action |
|
|
680
|
-
|-----------|--------|--------|
|
|
681
|
-
| `over=false` | `OK` | Post/refresh `FORGE:DIFF_SIZE`, continue to B6 |
|
|
682
|
-
| `over=true`, valid override | `OVERRIDDEN` | Post/refresh `FORGE:DIFF_SIZE` (quote the justification), continue to B6 |
|
|
683
|
-
| `over=true`, no override, issue body has a `**Parent**: #N` line OR a trusted `FORGE:DECOMPOSED` comment exists, AND `cohesive=true` | `COHESIVE` | Cohesive-overage path <!-- Added: forge#3790 -->: a sub-issue that is over the gate because one new module plus its tests dominate the diff (at least 70% of counted lines are new or test files, and `diff_lines` is at most `threshold` x 1.5, both computed by `diff-size.sh`) has no seam left to split and the loop guard would only strand it. Post/refresh `FORGE:DIFF_SIZE` with the cohesion note, do NOT add `needs-human`, continue to B6. Test lines are classified, never discounted. A `FORGE:SIZE_OVERRIDE` still takes precedence (`OVERRIDDEN`) |
|
|
684
|
-
| `over=true`, no override, issue body has a `**Parent**: #N` line OR a trusted `FORGE:DECOMPOSED` comment exists (and not `cohesive=true`) | `OVER` | Decompose-loop guard: do NOT route to decompose. Post/refresh `FORGE:DIFF_SIZE`, then Blocked exit with blocker `diff-size-over-threshold: {diff_lines} > {threshold}; post a FORGE:SIZE_OVERRIDE with a justification to proceed` |
|
|
685
|
-
| `over=true`, no override, otherwise | `OVER` | Put a `### Split Proposal` in the `FORGE:DIFF_SIZE` comment, then print `BUILD_RESULT: status: NEEDS_DECOMPOSE` (see below) |
|
|
686
|
-
|
|
687
|
-
**`### Split Proposal`** (written into the `FORGE:DIFF_SIZE` comment): list the top files by changed lines from the `top=` output, then propose 2-4 sub-issues split along directory or phase seams, each independently buildable and at or under `0.8 x threshold` (code plus tests; a seam that can't fit says so explicitly), in dependency order, one `- **{title}** — {files} — wip: {WIP_REF}@{WIP_SHA} (git checkout {WIP_SHA} -- {files})` line each (this is the plan `work-on:decompose` consumes in D0/D2). The `wip:` part comes from the persist step below; when persistence fell back to a patch file write `patch: {PATCH_PATH}` instead, and when both failed write `wip: unavailable`. Lines without the suffix stay valid.
|
|
688
|
-
|
|
689
|
-
**Persist the staged hunks** (before the `NEEDS_DECOMPOSE` exit; runs once the `FORGE:DIFF_SIZE` result is `OVER` and the decompose-loop guard did not fire) <!-- Added: forge#3573 -->: commit the staged index to a standalone commit object WITHOUT moving the build branch or the index, and push it to a dedicated ref named only from the issue number (never interpolate a title or file name into a ref). Re-running overwrites the same ref, so resume is idempotent. Never block `NEEDS_DECOMPOSE` on a persistence failure.
|
|
690
|
-
|
|
691
|
-
```bash
|
|
692
|
-
WIP_REF="forge/wip/{NUMBER}"
|
|
693
|
-
WIP_SHA=""; PATCH_PATH=""
|
|
694
|
-
TREE=$(git -C "{WORKTREE_PATH}" write-tree) \
|
|
695
|
-
&& WIP_SHA=$(git -C "{WORKTREE_PATH}" commit-tree "$TREE" -p HEAD -m "wip: staged hunks for #{NUMBER} (diff-size OVER)") || WIP_SHA=""
|
|
696
|
-
if [ -n "$WIP_SHA" ] && git -C "{WORKTREE_PATH}" push --force origin "${WIP_SHA}:refs/heads/${WIP_REF}" 2>/dev/null; then
|
|
697
|
-
echo "WIP_REF=${WIP_REF}@${WIP_SHA}"
|
|
698
|
-
else
|
|
699
|
-
PATCH_PATH="$(git -C "{WORKTREE_PATH}" rev-parse --git-common-dir)/forge-wip-{NUMBER}.patch"
|
|
700
|
-
git -C "{WORKTREE_PATH}" diff --cached --binary > "$PATCH_PATH" 2>/dev/null && echo "PATCH_PATH=${PATCH_PATH}" || echo "WIP=unavailable"
|
|
701
|
-
fi
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
The checkout command in each line restores that seam's files from the persisted commit (`git fetch origin {WIP_REF}` first in a fresh clone); `git diff {WIP_SHA}^ {WIP_SHA} -- {files}` shows them. Post `FORGE:DIFF_SIZE` (Step 3) after this step so the proposal carries the references.
|
|
705
|
-
|
|
706
|
-
**`NEEDS_DECOMPOSE` exit**: leave the staged worktree and branch as they are (the persisted WIP ref or patch is an additional durable copy, not a replacement); do not add `needs-human`; do not post `FORGE:BUILDER:COMPLETE`; do NOT call `work-on:decompose` here (the router is the sole dispatcher). The labels stay `workflow:building`; decompose owns the `workflow:decomposed` transition. Print:
|
|
707
|
-
|
|
708
|
-
```
|
|
709
|
-
BUILD_RESULT:
|
|
710
|
-
status: NEEDS_DECOMPOSE
|
|
711
|
-
branch: {BRANCH}
|
|
712
|
-
worktree: {WORKTREE_PATH}
|
|
713
|
-
blocker: diff-size-over-threshold: {diff_lines} > {threshold}; split proposal posted in FORGE:DIFF_SIZE
|
|
714
|
-
```
|
|
736
|
+
`SCOPE_NOTE` carries, one per line, `out_of_plan: <path>` for each out-of-plan file (up to 50), `overrun: <ws_lines> vs estimate <N> (>{OVERRUN_PCT}%)` when `overrun=true`, the `trimmed:` line after a trim, the override justification for `OVERRIDDEN`, and `review-note: <what reviewers should check>` (for example, check the out-of-plan files first). `DEPRECATION_NOTE` is `deprecated_config: build.diff_size.*` plus a newline when an alias supplied a value. A `FORGE:DIFF_SIZE` comment never contains a `### Split Proposal` heading and never routes anywhere: no engine path reads it as a decompose signal.
|
|
715
737
|
|
|
716
|
-
**Resume**:
|
|
717
|
-
# MUST CONTINUE to Phase B6
|
|
738
|
+
**Resume**: B5.5 is idempotent. A re-invoked build re-runs B5.5 only through the normal path (B0 resumes at B5/B6 by BUILDER/COMPLETE state); a repeated measure edits the same `FORGE:DIFF_SIZE` comment, and a repeated trim finds the out-of-plan files already gone from the diff.
|
|
739
|
+
# MUST CONTINUE to Phase B6 — the scope check result is intermediate, NOT terminal (validation still required), except the Blocked exits above.
|
|
718
740
|
|
|
719
741
|
---
|
|
720
742
|
|
|
@@ -1008,7 +1030,7 @@ Print this structured block as your final reply (and nothing after it) — the r
|
|
|
1008
1030
|
|
|
1009
1031
|
```
|
|
1010
1032
|
BUILD_RESULT:
|
|
1011
|
-
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE |
|
|
1033
|
+
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
1012
1034
|
branch: {BRANCH}
|
|
1013
1035
|
worktree: {WORKTREE_PATH}
|
|
1014
1036
|
blocker: {description if status=BLOCKED}
|
|
@@ -1017,8 +1039,7 @@ BUILD_RESULT:
|
|
|
1017
1039
|
- `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
1018
1040
|
- `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
|
|
1019
1041
|
- `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
|
|
1020
|
-
-
|
|
1021
|
-
- `COHESIVE` is not a `BUILD_RESULT` status: B5.5 records `result: COHESIVE` in `FORGE:DIFF_SIZE` for an over-gate sub-issue dominated by one new module plus tests, then the build continues to B6 and ends `COMPLETE`.
|
|
1042
|
+
- B5.5 never produces a `BUILD_RESULT` status of its own: its scope-check outcome (`OK`, `EXTENDED_REVIEW`, `SCOPE_FLAGGED`, `OVERRIDDEN`) is recorded in `FORGE:DIFF_SIZE`, then the build continues to B6 and ends `COMPLETE`. A finished build is never routed to decompose or discarded. <!-- Added: forge#3862 -->
|
|
1022
1043
|
- `BLOCKED` — any guard or failure; `needs-human` is set and the blocker is posted.
|
|
1023
1044
|
|
|
1024
1045
|
---
|
|
@@ -809,22 +809,55 @@ If no parent reference found → log a warning and skip this phase (`PARENT_STAT
|
|
|
809
809
|
echo "WARNING: No parent reference found in issue body — skipping parent tracker update"
|
|
810
810
|
```
|
|
811
811
|
|
|
812
|
-
If parent found:
|
|
812
|
+
If parent found, check this sub-issue off with a verified retry. The parent body is a shared read-modify-write target: a sibling lane closing at the same moment can overwrite this write from a stale copy and silently drop a checkbox. Each attempt re-reads the body, re-applies the substitution to the fresh copy, writes it, then re-reads to verify the box is present, retrying up to 3 times with backoff. Idempotent, so the resume path may re-enter it:
|
|
813
813
|
```bash
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
814
|
+
PARENT_UPDATED=false
|
|
815
|
+
for ATTEMPT in 1 2 3; do
|
|
816
|
+
# Fresh read immediately before each body mutation
|
|
817
|
+
PARENT_BODY=$(gh issue view "$PARENT_REF" {GH_FLAG} --json body --jq '.body')
|
|
818
|
+
# Already checked (earlier attempt, earlier run, or restored): nothing to write
|
|
819
|
+
if printf '%s\n' "$PARENT_BODY" | grep -qE "^[[:space:]]*[-*+] \[[xX]\] #{NUMBER}([^0-9]|\$)"; then PARENT_UPDATED=true; break; fi
|
|
820
|
+
# No checklist entry for this sub-issue: nothing to check off
|
|
821
|
+
printf '%s\n' "$PARENT_BODY" | grep -qE "^[[:space:]]*[-*+] \[ \] #{NUMBER}([^0-9]|\$)" || break
|
|
822
|
+
# Replace "- [ ] #{NUMBER}" with "- [x] #{NUMBER}"; the trailing guard stops #12 matching #123
|
|
823
|
+
UPDATED_PARENT=$(printf '%s\n' "$PARENT_BODY" | sed -E "s/- \[ \] #{NUMBER}([^0-9]|\$)/- [x] #{NUMBER}\1/g")
|
|
824
|
+
gh issue edit "$PARENT_REF" {GH_FLAG} --body "$UPDATED_PARENT" # allowlist:check-command-side-effects
|
|
825
|
+
# Verify the write survived: re-read and confirm the box is present
|
|
826
|
+
VERIFY_BODY=$(gh issue view "$PARENT_REF" {GH_FLAG} --json body --jq '.body')
|
|
827
|
+
if printf '%s\n' "$VERIFY_BODY" | grep -qE "^[[:space:]]*[-*+] \[[xX]\] #{NUMBER}([^0-9]|\$)"; then PARENT_UPDATED=true; break; fi
|
|
828
|
+
echo "WARNING: parent #${PARENT_REF} checkbox for #{NUMBER} not present after write (attempt ${ATTEMPT}/3) — retrying"
|
|
829
|
+
sleep "$ATTEMPT"
|
|
830
|
+
done
|
|
831
|
+
[ "$PARENT_UPDATED" = true ] || echo "WARNING: could not verify checkbox for #{NUMBER} on parent #${PARENT_REF} — completion is derived from sub-issue state"
|
|
832
|
+
```
|
|
821
833
|
|
|
822
|
-
|
|
823
|
-
|
|
834
|
+
Derive completion from sub-issue state, not from the checklist text, so a lost checkbox can never strand the parent. Every `#N` on a checklist line of a fresh parent read is looked up with `gh issue view N --json state`; this sub-issue is already CLOSED by C1/C2 and counts as closed. Unchecked lines without a leading `#N` ref (including a non-leading mention such as `(see #9)`) still count as open. If the re-read or any state lookup fails, or no refs are found, fall back to the checklist count (fail closed: a failed lookup is never treated as closed):
|
|
835
|
+
```bash
|
|
836
|
+
PARENT_FRESH=$(gh issue view "$PARENT_REF" {GH_FLAG} --json body --jq '.body' 2>/dev/null) || PARENT_FRESH="${UPDATED_PARENT:-${PARENT_BODY:-}}"
|
|
837
|
+
CHECKLIST_OPEN=$(printf '%s\n' "$PARENT_FRESH" | grep -cE '^[[:space:]]*[-*+] \[ \]' || true)
|
|
838
|
+
CHECKLIST_OPEN=${CHECKLIST_OPEN:-0}
|
|
839
|
+
SUB_REFS=$(printf '%s\n' "$PARENT_FRESH" | grep -E '^[[:space:]]*[-*+] \[[ xX]\] #[0-9]+' \
|
|
840
|
+
| sed -E 's/^[[:space:]]*[-*+] \[[ xX]\] #([0-9]+).*/\1/' | grep -vx "$PARENT_REF" | sort -u || true)
|
|
841
|
+
LOOKUP_FAILED=false; STATE_OPEN=0
|
|
842
|
+
for SUB in $SUB_REFS; do
|
|
843
|
+
[ "$SUB" = "{NUMBER}" ] && continue
|
|
844
|
+
SUB_STATE=$(gh issue view "$SUB" {GH_FLAG} --json state --jq '.state' 2>/dev/null) || SUB_STATE=""
|
|
845
|
+
case "$SUB_STATE" in
|
|
846
|
+
CLOSED) ;;
|
|
847
|
+
OPEN) STATE_OPEN=$((STATE_OPEN + 1)) ;;
|
|
848
|
+
*) LOOKUP_FAILED=true ;;
|
|
849
|
+
esac
|
|
850
|
+
done
|
|
851
|
+
if [ "$LOOKUP_FAILED" = true ] || [ -z "$SUB_REFS" ]; then
|
|
852
|
+
OPEN_SUBS=$CHECKLIST_OPEN
|
|
853
|
+
else
|
|
854
|
+
NOREF_OPEN=$(printf '%s\n' "$PARENT_FRESH" | grep -E '^[[:space:]]*[-*+] \[ \]' | grep -cvE '^[[:space:]]*[-*+] \[ \] #[0-9]+' || true)
|
|
855
|
+
OPEN_SUBS=$((STATE_OPEN + ${NOREF_OPEN:-0}))
|
|
856
|
+
fi
|
|
824
857
|
OPEN_SUBS=${OPEN_SUBS:-0}
|
|
825
858
|
```
|
|
826
859
|
|
|
827
|
-
If `OPEN_SUBS == 0` (all sub-issues
|
|
860
|
+
If `OPEN_SUBS == 0` (all sub-issues done), 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
861
|
```bash
|
|
829
862
|
if [ "$OPEN_SUBS" -eq 0 ]; then
|
|
830
863
|
PARENT_STATE=$(gh issue view "$PARENT_REF" {GH_FLAG} --json state --jq '.state' 2>/dev/null || echo OPEN)
|
|
@@ -970,15 +1003,17 @@ First read the build's diff size (forge#3450). `FORGE:DIFF_SIZE` is posted by `w
|
|
|
970
1003
|
|
|
971
1004
|
```bash
|
|
972
1005
|
# <Script resolution block, verbatim>
|
|
973
|
-
DIFF_LINES="—";
|
|
1006
|
+
DIFF_LINES="—"; REVIEW_WEIGHT="—"; SCOPE_RESULT="—"; SIZE_OVERRIDE_USED="no"
|
|
974
1007
|
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
975
1008
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ]; then
|
|
976
1009
|
_ds=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" 2>/dev/null \
|
|
977
1010
|
| bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE' 2>/dev/null | tail -n 1 | jq -r 'select(type == "string")' 2>/dev/null) || _ds=""
|
|
978
1011
|
_n=$(printf '%s\n' "$_ds" | sed -n 's/^diff_lines: *\([0-9][0-9]*\).*/\1/p' | head -n 1)
|
|
979
|
-
_t=$(printf '%s\n' "$_ds" | sed -n 's/^
|
|
1012
|
+
_t=$(printf '%s\n' "$_ds" | sed -n 's/^review_weight: *\([0-9][0-9]*\).*/\1/p' | head -n 1)
|
|
1013
|
+
_r=$(printf '%s\n' "$_ds" | sed -n 's/^result: *\([A-Z_]*\).*/\1/p' | head -n 1)
|
|
980
1014
|
[ -n "$_n" ] && DIFF_LINES="$_n"
|
|
981
|
-
[ -n "$_t" ] &&
|
|
1015
|
+
[ -n "$_t" ] && REVIEW_WEIGHT="$_t"
|
|
1016
|
+
[ -n "$_r" ] && SCOPE_RESULT="$_r"
|
|
982
1017
|
printf '%s\n' "$_ds" | grep -q '^result: *OVERRIDDEN' && SIZE_OVERRIDE_USED="yes"
|
|
983
1018
|
fi
|
|
984
1019
|
```
|
|
@@ -1182,7 +1217,7 @@ ${DECISIONS_BLOCK}
|
|
|
1182
1217
|
|
|
1183
1218
|
**Cost (economic scheduling)**: ${COST_DELTA_NOTE}
|
|
1184
1219
|
|
|
1185
|
-
**Diff size**: ${DIFF_LINES:-—} lines (
|
|
1220
|
+
**Diff size**: ${DIFF_LINES:-—} lines (review weight ${REVIEW_WEIGHT:-—}, scope check ${SCOPE_RESULT:-—}, override: ${SIZE_OVERRIDE_USED:-no})
|
|
1186
1221
|
|
|
1187
1222
|
**Pipeline completed**: ${TIMESTAMP}
|
|
1188
1223
|
|