forgedock 1.7.1 → 1.8.1
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/admission.mjs +242 -7
- package/bin/engine/admission.test.mjs +183 -2
- package/bin/engine/phases.mjs +31 -22
- package/bin/engine/resolve.mjs +8 -1
- package/bin/engine/resolve.test.mjs +5 -0
- package/bin/engine.mjs +26 -17
- package/bin/forgedock.mjs +15 -2
- package/bin/hooks/pre-tool-use.mjs +573 -2
- package/bin/journey.mjs +11 -7
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +47 -13
- package/bin/orchestrate-preflight.mjs +434 -0
- package/bin/runner.mjs +30 -4
- package/commands/audit-agents.md +75 -26
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +10 -5
- package/commands/issue.md +95 -35
- package/commands/orchestrate/phase-1-resolve.md +70 -32
- package/commands/orchestrate/phase-3-dependency.md +141 -13
- package/commands/orchestrate/phase-4-execution.md +660 -70
- package/commands/orchestrate/phase-5-cleanup.md +5 -11
- package/commands/orchestrate/phase-6-report.md +27 -1
- package/commands/orchestrate/safety.md +1 -1
- package/commands/orchestrate.md +39 -1
- package/commands/quality-gate.md +30 -10
- package/commands/review-pr-agents/protocols.md +13 -0
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +100 -18
- package/commands/review-pr.md +78 -24
- package/commands/upgrade-deps.md +1 -1
- package/commands/work-on/build/validate.md +25 -4
- package/commands/work-on/close.md +40 -10
- package/commands/work-on/decompose.md +33 -1
- package/commands/work-on/investigate.md +33 -0
- package/commands/work-on/remediate.md +40 -5
- package/commands/work-on/review.md +27 -0
- package/commands/work-on.md +48 -5
- package/docs/CONFIG.md +45 -10
- package/docs/OPENCODE.md +21 -6
- package/docs/spec/review-protocol.md +7 -0
- package/package.json +1 -1
- package/scripts/check-admission-parity.mjs +1 -0
- package/scripts/check-command-side-effects.sh +14 -13
- package/scripts/check-shell-portability.sh +36 -0
- package/scripts/check-spec-markers.sh +21 -11
- package/scripts/checkbox-sections.test.sh +67 -0
- package/scripts/extract-affected-files.sh +85 -5
- package/scripts/extract-affected-files.test.sh +154 -2
|
@@ -189,6 +189,8 @@ should_dispatch() {
|
|
|
189
189
|
|
|
190
190
|
**WHY THIS EXISTS** <!-- Added: forge#1912 -->: Phase 4's dispatch loops previously launched every DAG-ready issue in one shot — the engine-first bash loop backgrounded every `should_dispatch()`-passing issue with no count limit, and the Agent-spawn-fallback path was explicitly instructed to "launch all ready agents simultaneously." On a large ready set this saturates the Anthropic API rate limit in one burst, causing cascading failures across most of the batch. This is a distinct, count-denominated gate from the dollar-denominated `--budget` gate above (Step 4A-pre.0) — an issue can be deferred for budget reasons, concurrency reasons, or both; each gate accumulates its own deferred list independently.
|
|
191
191
|
|
|
192
|
+
`MAX_CONCURRENT` caps top-level `/work-on` dispatches, not the total number of harness subagents. A normal worker can spawn context, architect, implement, validate, quality-gate, and review children. Budget **8 total subagent spawns per worker** when choosing this value: `max_concurrent <= session_subagent_budget / 8`. This is a conservative planning multiplier, not a second runtime semaphore; the harness does not expose a reliable cross-agent spawn counter to this shell-level dispatcher.
|
|
193
|
+
|
|
192
194
|
Initialize the concurrency cap and its batch-scope tracking state before the first dispatch:
|
|
193
195
|
|
|
194
196
|
```bash
|
|
@@ -209,8 +211,12 @@ fi
|
|
|
209
211
|
|
|
210
212
|
ACTIVE_DISPATCH_COUNT=0 # in-flight dispatched-but-not-yet-completed agents, this batch
|
|
211
213
|
DEFERRED_CONCURRENCY_ISSUES=() # ready issues held back because no headroom was available
|
|
214
|
+
SUBAGENT_SPAWN_BUDGET_PER_WORKER=8 # /work-on + build/review fan-out planning estimate
|
|
215
|
+
TOP_LEVEL_DISPATCH_TOTAL=0
|
|
216
|
+
PLANNED_SUBAGENT_SPAWNS=0
|
|
217
|
+
OBSERVED_SUBAGENT_SPAWNS="unavailable" # replace only with harness telemetry; never infer
|
|
212
218
|
|
|
213
|
-
echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT} (
|
|
219
|
+
echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT}; plan for up to $((MAX_CONCURRENT * SUBAGENT_SPAWN_BUDGET_PER_WORKER)) subagent spawns in flight (8 per worker)"
|
|
214
220
|
# --- End concurrency gate initialization ---
|
|
215
221
|
```
|
|
216
222
|
|
|
@@ -226,6 +232,31 @@ dispatch_headroom() {
|
|
|
226
232
|
|
|
227
233
|
**This is a hard cap, not a suggestion.** No dispatch site in Phase 4 may background/spawn more than `dispatch_headroom` new agents without first waiting for in-flight agents to complete. Issues that cannot be dispatched due to the cap go into `DEFERRED_CONCURRENCY_ISSUES[]` and are released — in the order they were deferred — as running agents complete (Step 4B), the same event-driven model (no sleep/poll loops) the file already uses for DAG-readiness and budget gating.
|
|
228
234
|
|
|
235
|
+
**Dispatch accounting**: Immediately after each successful top-level dispatch, increment `TOP_LEVEL_DISPATCH_TOTAL` and add `SUBAGENT_SPAWN_BUDGET_PER_WORKER` to `PLANNED_SUBAGENT_SPAWNS`. When the runtime reports child-spawn telemetry, replace `OBSERVED_SUBAGENT_SPAWNS` with that measured total; otherwise leave it `unavailable` rather than fabricating a measured value.
|
|
236
|
+
|
|
237
|
+
### Step 4A-pre.0.3: Secondary content-creation backpressure (MANDATORY)
|
|
238
|
+
|
|
239
|
+
GitHub's `resources.core` quota does not report secondary content-creation throttles. Treat any GitHub API response with HTTP `403` whose body contains `secondary rate limit` as a batch-wide backpressure signal.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Call from every dispatcher-owned gh write/create wrapper and from a child completion
|
|
243
|
+
# report that includes its final GitHub API error. Do not retry the failed request here.
|
|
244
|
+
SECONDARY_RATE_LIMITED=false
|
|
245
|
+
SECONDARY_RATE_LIMIT_MESSAGE=""
|
|
246
|
+
|
|
247
|
+
record_secondary_rate_limit() {
|
|
248
|
+
local STATUS="$1"
|
|
249
|
+
local BODY="$2"
|
|
250
|
+
if [ "$STATUS" = "403" ] && echo "$BODY" | grep -qi 'secondary rate limit'; then
|
|
251
|
+
SECONDARY_RATE_LIMITED=true
|
|
252
|
+
SECONDARY_RATE_LIMIT_MESSAGE="$BODY"
|
|
253
|
+
echo "SECONDARY RATE LIMIT: pausing all new dispatch. Do not retry GitHub content creation until an operator resumes the batch." >&2
|
|
254
|
+
fi
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Before every dispatch site computes headroom or launches a worker, check `SECONDARY_RATE_LIMITED`. When true, preserve the ready/deferred queues, launch no more workers, and end the active dispatch cycle. Do not poll or retry GitHub calls to discover recovery: an operator resumes the batch after the throttle clears. In-flight agents receive this contract in the dispatch prompt: on a secondary-limit 403, they must stop their GitHub write/create retry loop, report the response to the orchestrator, and exit their current phase without creating replacement work.
|
|
259
|
+
|
|
229
260
|
### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
|
|
230
261
|
|
|
231
262
|
**WHY THIS EXISTS**: Milestone-code-onto-staging contamination incidents (see issue #150) produce unexpected growth on the staging branch that is otherwise invisible until after a deploy. In the streaming DAG model, there are no discrete wave boundaries — instead, track a running baseline and check after each agent completion.
|
|
@@ -409,6 +440,16 @@ echo "Step 4A.pre: using BATCH_T0=${BATCH_T0} for Step 4C's run-spawned cascade
|
|
|
409
440
|
DEFERRED_FINDINGS=()
|
|
410
441
|
QUEUED_FINDINGS=()
|
|
411
442
|
declare -A DEFERRED_REASONS
|
|
443
|
+
# Count every finding regardless of admission, so a deferred refinement never
|
|
444
|
+
# makes the reported amplification ratio look better than it was.
|
|
445
|
+
FINDINGS_SPAWNED=0
|
|
446
|
+
MERGED_UNITS=0
|
|
447
|
+
AMPLIFICATION_RATIO_HISTORY=()
|
|
448
|
+
AMPLIFICATION_DEFERRED=()
|
|
449
|
+
declare -A FINDINGS_BY_SOURCE_PR
|
|
450
|
+
declare -A REFINEMENT_FINDINGS
|
|
451
|
+
declare -A NEW_SURFACE_FINDINGS
|
|
452
|
+
declare -A AMPLIFICATION_FINDING_SEEN
|
|
412
453
|
declare -A AGENT_ISSUE_MAP
|
|
413
454
|
# Engine-first dispatch equivalent of AGENT_ISSUE_MAP (fixed forge#2466): keyed by issue
|
|
414
455
|
# number, holds the task id returned by each backgrounded `Bash(run_in_background=true,
|
|
@@ -417,9 +458,29 @@ declare -A AGENT_ISSUE_MAP
|
|
|
417
458
|
# the same role AGENT_ISSUE_MAP plays for Agent-tool `agent_completed` notifications.
|
|
418
459
|
declare -A ENGINE_DISPATCH_MAP
|
|
419
460
|
# OpenCode native task equivalent of ENGINE_DISPATCH_MAP. Keys are issue numbers;
|
|
420
|
-
# values are the session ids returned by task(background=true).
|
|
461
|
+
# values are the session ids returned by task(background=true). This is a live
|
|
462
|
+
# cache only; every entry must also be persisted as a FORGE:DISPATCH comment.
|
|
421
463
|
declare -A OPENCODE_DISPATCH_MAP
|
|
422
464
|
|
|
465
|
+
# Reconstruct the live map after compaction/restart from the durable dispatch
|
|
466
|
+
# records. The issue association is the map key; only the latest running record
|
|
467
|
+
# is restored, because a later completed/error record has already released it.
|
|
468
|
+
for NUM in "${ISSUES[@]}"; do
|
|
469
|
+
DISPATCH_BODY=$(gh api repos/{GH_REPO}/issues/"$NUM"/comments \
|
|
470
|
+
--jq '[.[] | select(.body | contains("FORGE:DISPATCH")) | .body] | last // ""' 2>/dev/null || true)
|
|
471
|
+
TASK_ID=$(printf '%s' "$DISPATCH_BODY" | jq -Rr 'try capture("\\\"runtime\\\":\\\"opencode\\\",\\\"child_session_id\\\":\\\"(?<id>[^\\\"]+)\\\".*\\\"state\\\":\\\"running\\\"").id catch ""')
|
|
472
|
+
if [ -n "$TASK_ID" ]; then
|
|
473
|
+
OPENCODE_DISPATCH_MAP["$NUM"]="$TASK_ID"
|
|
474
|
+
fi
|
|
475
|
+
done
|
|
476
|
+
|
|
477
|
+
# Full-repository intake is intentionally separate from Step 4C's T0-scoped
|
|
478
|
+
# review-finding cascade. It is enabled only for an operator-authorised
|
|
479
|
+
# policy: all run; records every open issue already observed so CI-created
|
|
480
|
+
# actionable work is admitted once rather than rediscovered every cycle.
|
|
481
|
+
declare -A SEEN_OPEN_ISSUES
|
|
482
|
+
FULL_REPO_SWEEP_COUNT=0
|
|
483
|
+
|
|
423
484
|
# Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
|
|
424
485
|
# dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
|
|
425
486
|
# EDGE_KIND/EDGE_FILES from phase-3-dependency.md Step 3C) resolves; consumed by Step 4A's
|
|
@@ -428,6 +489,42 @@ declare -A OPENCODE_DISPATCH_MAP
|
|
|
428
489
|
declare -A SAME_FILE_BRIEF
|
|
429
490
|
declare -A EDGE_BRIEFED
|
|
430
491
|
|
|
492
|
+
# DONE-path edge re-verification memo (forge#2848). A conclusive re-derivation is
|
|
493
|
+
# memoized in EDGE_REDERIVED; inconclusive results get one retry, capped by
|
|
494
|
+
# EDGE_REDERIVE_ATTEMPTS. This avoids caching a transient failure while bounding
|
|
495
|
+
# extraction to two attempts per descendant per batch.
|
|
496
|
+
declare -A EDGE_REDERIVED
|
|
497
|
+
declare -A EDGE_REDERIVE_ATTEMPTS
|
|
498
|
+
|
|
499
|
+
# Affected-file extraction helper, resolved for the DONE-path cohort re-derivation
|
|
500
|
+
# (forge#2848). Same resolver precedence as phase-3-dependency.md Step 3C Layer 1 —
|
|
501
|
+
# ForgeDock's runtime installation before the target repository — because the
|
|
502
|
+
# orchestrator runs inside the project being worked on, where a bare
|
|
503
|
+
# `bash scripts/extract-affected-files.sh` silently fails when that project has not
|
|
504
|
+
# copied ForgeDock's helper scripts into its own repository (#2794/#2791).
|
|
505
|
+
resolve_extract_affected_files() {
|
|
506
|
+
local candidates=()
|
|
507
|
+
[ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/extract-affected-files.sh")
|
|
508
|
+
[ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/extract-affected-files.sh")
|
|
509
|
+
candidates+=("$PWD/scripts/extract-affected-files.sh")
|
|
510
|
+
|
|
511
|
+
local candidate
|
|
512
|
+
for candidate in "${candidates[@]}"; do
|
|
513
|
+
if [ -f "$candidate" ]; then
|
|
514
|
+
printf '%s\n' "$candidate"
|
|
515
|
+
return 0
|
|
516
|
+
fi
|
|
517
|
+
done
|
|
518
|
+
echo "ERROR: extract-affected-files.sh is not installed in any configured runtime path." >&2
|
|
519
|
+
return 1
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Non-fatal here, unlike Phase 3's hard exit: Phase 3 cannot build a DAG at all without
|
|
523
|
+
# this helper, whereas Phase 4 only needs it for the OPTIONAL re-derivation optimization.
|
|
524
|
+
# If it is missing, re-derivation is skipped and edges stay as planned — the conservative
|
|
525
|
+
# direction, and identical to this file's pre-forge#2848 behavior.
|
|
526
|
+
AFFECTED_FILES_SCRIPT=$(resolve_extract_affected_files) || AFFECTED_FILES_SCRIPT=""
|
|
527
|
+
|
|
431
528
|
# Human-gated idle/backpressure flag (Step 4B item 6.7, forge#1814). Starts false —
|
|
432
529
|
# recomputed every completion cycle over {all_batch_issue_numbers}. Declared at batch
|
|
433
530
|
# scope (not per-agent) so Step 4C can read the latest value on every iteration.
|
|
@@ -452,15 +549,15 @@ CASCADE_POLICY_NAME=$(yq '.orchestration.cascade.policy // "balanced"' forge.yam
|
|
|
452
549
|
|
|
453
550
|
case "$CASCADE_POLICY_NAME" in
|
|
454
551
|
all)
|
|
455
|
-
PRESET_MAX_GEN="unlimited"; PRESET_TOKEN_BUDGET="unlimited"; PRESET_DEFER_GATED="false"; PRESET_KEYWORD="false"; PRESET_P3_SAME_FILE="false" ;;
|
|
552
|
+
PRESET_MAX_GEN="unlimited"; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET="unlimited"; PRESET_DEFER_GATED="false"; PRESET_KEYWORD="false"; PRESET_P3_SAME_FILE="false" ;;
|
|
456
553
|
conservative)
|
|
457
|
-
PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=450000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
554
|
+
PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=450000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
458
555
|
balanced)
|
|
459
|
-
PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
556
|
+
PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
460
557
|
*)
|
|
461
558
|
echo "WARNING: forge.yaml → orchestration.cascade.policy \"${CASCADE_POLICY_NAME}\" is not one of: all, balanced, conservative — falling back to \"balanced\""
|
|
462
559
|
CASCADE_POLICY_NAME="balanced"
|
|
463
|
-
PRESET_MAX_GEN=1; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
560
|
+
PRESET_MAX_GEN=1; PRESET_BATCH_MAX_GEN=2; PRESET_TOKEN_BUDGET=900000; PRESET_DEFER_GATED="true"; PRESET_KEYWORD="true"; PRESET_P3_SAME_FILE="true" ;;
|
|
464
561
|
esac
|
|
465
562
|
|
|
466
563
|
# max_generation is authoritatively resolved in phase-1-resolve.md (it only governs
|
|
@@ -473,6 +570,14 @@ if [ "$MAX_GENERATION_FOR_NOTICE" != "unlimited" ] && ! echo "$MAX_GENERATION_FO
|
|
|
473
570
|
MAX_GENERATION_FOR_NOTICE="$PRESET_MAX_GEN"
|
|
474
571
|
fi
|
|
475
572
|
|
|
573
|
+
# P3 batching is a distinct, bounded aggregation exception to autonomous cascade
|
|
574
|
+
# admission. Unlike max_generation, it must always be finite, including policy: all.
|
|
575
|
+
BATCH_MAX_GENERATION=$(yq ".orchestration.cascade.batch_max_generation // ${PRESET_BATCH_MAX_GEN}" forge.yaml 2>/dev/null || echo "$PRESET_BATCH_MAX_GEN")
|
|
576
|
+
if ! echo "$BATCH_MAX_GENERATION" | grep -qE '^[1-9][0-9]*$'; then
|
|
577
|
+
echo "WARNING: forge.yaml → orchestration.cascade.batch_max_generation is not a positive integer (\"${BATCH_MAX_GENERATION}\") — falling back to default ${PRESET_BATCH_MAX_GEN}"
|
|
578
|
+
BATCH_MAX_GENERATION="$PRESET_BATCH_MAX_GEN"
|
|
579
|
+
fi
|
|
580
|
+
|
|
476
581
|
# token_budget precedence: orchestration.cascade.token_budget (new home) >
|
|
477
582
|
# pipeline.token_budget_per_batch (deprecated alias, forge#1858, kept working
|
|
478
583
|
# unchanged) > the resolved preset default. Accepts the "unlimited" sentinel —
|
|
@@ -491,13 +596,30 @@ fi
|
|
|
491
596
|
|
|
492
597
|
TOKEN_ESTIMATE_PER_FINDING=$(yq '.pipeline.token_estimate_per_finding // 150000' forge.yaml 2>/dev/null || echo 150000)
|
|
493
598
|
|
|
599
|
+
# Amplification is observational by default. The optional ceiling is evaluated
|
|
600
|
+
# only against same-lineage refinements in Step 4C, never against new surface.
|
|
601
|
+
CASCADE_MAX_AMPLIFICATION=$(yq '.orchestration.cascade.max_amplification // "off"' forge.yaml 2>/dev/null || echo "off")
|
|
602
|
+
if [ "$CASCADE_MAX_AMPLIFICATION" != "off" ]; then
|
|
603
|
+
if ! echo "$CASCADE_MAX_AMPLIFICATION" | grep -qE '^[0-9]+(\.[0-9]+)?$' || \
|
|
604
|
+
! awk "BEGIN { exit !($CASCADE_MAX_AMPLIFICATION > 0) }"; then
|
|
605
|
+
echo "WARNING: forge.yaml → orchestration.cascade.max_amplification must be a positive number or off — disabling the bound"
|
|
606
|
+
CASCADE_MAX_AMPLIFICATION="off"
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
CONVERGENCE_WINDOW=$(yq '.orchestration.cascade.convergence_window // 3' forge.yaml 2>/dev/null || echo 3)
|
|
610
|
+
if ! echo "$CONVERGENCE_WINDOW" | grep -qE '^[1-9][0-9]*$'; then
|
|
611
|
+
echo "WARNING: forge.yaml → orchestration.cascade.convergence_window must be a positive integer — falling back to 3"
|
|
612
|
+
CONVERGENCE_WINDOW=3
|
|
613
|
+
fi
|
|
614
|
+
echo "Cascade amplification: max_amplification=${CASCADE_MAX_AMPLIFICATION} convergence_window=${CONVERGENCE_WINDOW} (off preserves current admission behavior)"
|
|
615
|
+
|
|
494
616
|
# Independent boolean levers — each accepts an explicit granular override on
|
|
495
617
|
# top of the resolved preset (preset supplies the default, not a hard value).
|
|
496
618
|
CASCADE_DEFER_ON_BATCH_GATED=$(yq ".orchestration.cascade.defer_on_batch_gated // ${PRESET_DEFER_GATED}" forge.yaml 2>/dev/null || echo "$PRESET_DEFER_GATED")
|
|
497
619
|
CASCADE_KEYWORD_HEURISTIC=$(yq ".orchestration.cascade.keyword_heuristic // ${PRESET_KEYWORD}" forge.yaml 2>/dev/null || echo "$PRESET_KEYWORD")
|
|
498
620
|
CASCADE_P3_SAME_FILE_DEFER=$(yq ".orchestration.cascade.p3_same_file_defer // ${PRESET_P3_SAME_FILE}" forge.yaml 2>/dev/null || echo "$PRESET_P3_SAME_FILE")
|
|
499
621
|
|
|
500
|
-
echo "Cascade admission policy resolved: policy=${CASCADE_POLICY_NAME} token_budget=${TOKEN_BUDGET} defer_on_batch_gated=${CASCADE_DEFER_ON_BATCH_GATED} keyword_heuristic=${CASCADE_KEYWORD_HEURISTIC} p3_same_file_defer=${CASCADE_P3_SAME_FILE_DEFER} (forge.yaml → orchestration.cascade; see docs/CONFIG.md)"
|
|
622
|
+
echo "Cascade admission policy resolved: policy=${CASCADE_POLICY_NAME} token_budget=${TOKEN_BUDGET} batch_max_generation=${BATCH_MAX_GENERATION} defer_on_batch_gated=${CASCADE_DEFER_ON_BATCH_GATED} keyword_heuristic=${CASCADE_KEYWORD_HEURISTIC} p3_same_file_defer=${CASCADE_P3_SAME_FILE_DEFER} (forge.yaml → orchestration.cascade; see docs/CONFIG.md)"
|
|
501
623
|
|
|
502
624
|
# Both-uncapped notice (loud, one-time, printed once per orchestrate invocation since
|
|
503
625
|
# this resolution block itself runs once at Step 4A.pre) — never a preset default except
|
|
@@ -524,6 +646,8 @@ TOKEN_DEFERRED=() # findings deferred by the token-budget rule this run (re-ev
|
|
|
524
646
|
# alone. <!-- Added: forge#1909 -->
|
|
525
647
|
SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch across the run
|
|
526
648
|
SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
|
|
649
|
+
BATCHABLE_DEFERRED_P3=() # gen-2+ P3s eligible only for bounded P3 aggregation (forge#2849)
|
|
650
|
+
declare -A FINDING_GENERATIONS
|
|
527
651
|
|
|
528
652
|
for NUM in {ready_issue_numbers}; do
|
|
529
653
|
PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$NUM" -R {GH_REPO}) || {
|
|
@@ -547,6 +671,97 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
|
|
|
547
671
|
|
|
548
672
|
### Step 4A: Dispatch ready issues
|
|
549
673
|
|
|
674
|
+
### Step 4A.0: Probe Knowledge Gist capability once
|
|
675
|
+
|
|
676
|
+
Probe the authenticated identity once for this orchestration run before any engine, Agent, or
|
|
677
|
+
OpenCode worker is dispatched. A GitHub App installation token identifies as `Bot` and cannot use
|
|
678
|
+
the Gists API; cache that fact rather than letting every worker rediscover it by attempting a
|
|
679
|
+
write. An unavailable identity probe preserves existing behavior for PAT-authenticated runs.
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
if [ -z "${FORGE_GIST_CAPABLE+x}" ]; then
|
|
683
|
+
GIST_AUTH_TYPE=$(gh api user --jq '.type' 2>/dev/null || true)
|
|
684
|
+
if [ "$GIST_AUTH_TYPE" = "Bot" ]; then
|
|
685
|
+
FORGE_GIST_CAPABLE=false
|
|
686
|
+
else
|
|
687
|
+
FORGE_GIST_CAPABLE=true
|
|
688
|
+
fi
|
|
689
|
+
export FORGE_GIST_CAPABLE
|
|
690
|
+
fi
|
|
691
|
+
|
|
692
|
+
if [ "$FORGE_GIST_CAPABLE" = "true" ]; then
|
|
693
|
+
echo "Knowledge Gist capability available"
|
|
694
|
+
else
|
|
695
|
+
echo "INFO: Knowledge Gist subsystem unavailable for this authentication; workers will skip it"
|
|
696
|
+
fi
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Carry `FORGE_GIST_CAPABLE` unchanged through every dispatch path. Do not probe it in individual
|
|
700
|
+
workers dispatched by this run. Phase 6 reports a false value once at batch level.
|
|
701
|
+
|
|
702
|
+
**Claims-board dispatch gate (MANDATORY, before every individual dispatch)** <!-- Added: forge#2844 -->: The coordination issue is the durable authority for file ownership. Do not use `EDGE_FILES`, `ISSUE_FILES`, or a remembered prior read as evidence that a claim is free. Immediately before dispatching each issue, re-read the full claims board and refuse that dispatch when the issue's declared file set intersects a live claim held by another issue. This applies equally to engine, Claude Agent, and OpenCode task dispatches, including newly-ready issues and wake reconstruction.
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
# Returns unreleased claims as [{holder: "123", files: "..."}]. A release is a separate,
|
|
706
|
+
# later comment, so it must be paired with its claim by holder rather than searched for in the
|
|
707
|
+
# claim comment itself. Terminal holder states also self-heal claims left behind by dead agents.
|
|
708
|
+
read_active_claims() {
|
|
709
|
+
local COORD_NUM="$1"
|
|
710
|
+
local CLAIMS HOLDER TERMINAL
|
|
711
|
+
CLAIMS=$(gh api --paginate --slurp "repos/{GH_REPO}/issues/${COORD_NUM}/comments" 2>/dev/null \
|
|
712
|
+
| jq -c '
|
|
713
|
+
flatten as $comments |
|
|
714
|
+
[$comments[]
|
|
715
|
+
| select(.body | contains("<!-- FORGE:CLAIM -->"))
|
|
716
|
+
| . as $claim
|
|
717
|
+
| ($claim.body | capture("\\*\\*Holder\\*\\*: #(?<holder>[0-9]+)").holder) as $holder
|
|
718
|
+
| select([$comments[]
|
|
719
|
+
| select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->"))
|
|
720
|
+
| select((.body | capture("\\*\\*Holder\\*\\*: #(?<holder>[0-9]+)").holder) == $holder)
|
|
721
|
+
| select(.created_at > $claim.created_at)] | length == 0)
|
|
722
|
+
| {holder: $holder,
|
|
723
|
+
files: ($claim.body | capture("\\*\\*Files\\*\\*: (?<files>[\\s\\S]*?)(?:\\n\\*\\*Interfaces\\*\\*:|$)").files)}]') || return 1
|
|
724
|
+
|
|
725
|
+
for HOLDER in $(echo "$CLAIMS" | jq -r '.[].holder'); do
|
|
726
|
+
TERMINAL=$(gh issue view "$HOLDER" -R {GH_REPO} --json labels --jq \
|
|
727
|
+
'[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:awaiting-merge" or . == "needs-human")] | length > 0' 2>/dev/null)
|
|
728
|
+
[ "$TERMINAL" = "true" ] && CLAIMS=$(echo "$CLAIMS" | jq --arg holder "$HOLDER" '[.[] | select(.holder != $holder)]')
|
|
729
|
+
done
|
|
730
|
+
echo "$CLAIMS"
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
claim_conflicts_with_live_holder() {
|
|
734
|
+
local NUM="$1" TARGET_FILES CLAIM HOLDER CLAIM_FILES OVERLAP
|
|
735
|
+
TARGET_FILES=$(printf '%s\n' "${ISSUE_FILES[$NUM]:-}" | sed -E '/^[[:space:]]*$/d; s/^[[:space:]]*[-*][[:space:]]*//; s/`//g' | sort -u)
|
|
736
|
+
[ -z "$TARGET_FILES" ] && return 1 # No declared paths: normal conservative DAG rules still apply.
|
|
737
|
+
|
|
738
|
+
ACTIVE_CLAIMS=$(read_active_claims "$COORD_ISSUE_NUMBER") || {
|
|
739
|
+
echo "REFUSING TO DISPATCH #${NUM}: could not read the durable claims board." >&2
|
|
740
|
+
return 0
|
|
741
|
+
}
|
|
742
|
+
while IFS= read -r CLAIM; do
|
|
743
|
+
HOLDER=$(echo "$CLAIM" | jq -r '.holder')
|
|
744
|
+
[ "$HOLDER" = "$NUM" ] && continue
|
|
745
|
+
CLAIM_FILES=$(echo "$CLAIM" | jq -r '.files' | sed -E '/^[[:space:]]*$/d; s/^[[:space:]]*[-*][[:space:]]*//; s/`//g' | sort -u)
|
|
746
|
+
OVERLAP=$(comm -12 <(printf '%s\n' "$TARGET_FILES") <(printf '%s\n' "$CLAIM_FILES"))
|
|
747
|
+
if [ -n "$OVERLAP" ]; then
|
|
748
|
+
echo "DEFERRING #${NUM}: declared files overlap live claim held by #${HOLDER}: ${OVERLAP//$'\n'/, }"
|
|
749
|
+
return 0
|
|
750
|
+
fi
|
|
751
|
+
done < <(echo "$ACTIVE_CLAIMS" | jq -c '.[]')
|
|
752
|
+
return 1
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
# Run this immediately before each engine Bash, Agent(), or OpenCode task call. Do not batch the
|
|
756
|
+
# board read: another worker can claim a file between two dispatches in the same ready set.
|
|
757
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
|
|
758
|
+
if claim_conflicts_with_live_holder "$NUM"; then
|
|
759
|
+
DEFERRED_CLAIM_ISSUES+=("$NUM")
|
|
760
|
+
continue
|
|
761
|
+
fi
|
|
762
|
+
fi
|
|
763
|
+
```
|
|
764
|
+
|
|
550
765
|
**Engine-first dispatch (default)**: When `forgedock` is in PATH, dispatch each ready issue via the durable engine rather than spawning prose Agent sub-agents. The engine's phase table enforces gate semantics in code — its fail-closed review gate and deterministic phase ordering are not subject to LLM interpretation.
|
|
551
766
|
|
|
552
767
|
**OpenCode dispatch (runtime-neutral adapter)**: When `FORGE_RUNTIME=opencode` or
|
|
@@ -567,14 +782,26 @@ task(
|
|
|
567
782
|
description="Work on {PROJECT_PREFIX}#{NUMBER}",
|
|
568
783
|
subagent_type="general",
|
|
569
784
|
background=true,
|
|
570
|
-
prompt="Use the same Phase 4A work-on template below. Invoke Skill(skill='work-on', args='{PROJECT_PREFIX}{NUMBER} --under-orchestration') and continue until a terminal workflow state."
|
|
785
|
+
prompt="Use the same Phase 4A work-on template below. Before invoking it, run `export FORGE_GIST_CAPABLE={FORGE_GIST_CAPABLE}` so the cached orchestration capability is preserved. Invoke Skill(skill='work-on', args='{PROJECT_PREFIX}{NUMBER} --under-orchestration') and continue until a terminal workflow state."
|
|
571
786
|
)
|
|
572
787
|
```
|
|
573
788
|
|
|
574
789
|
Capture the returned `<task id="..." state="running">` id in
|
|
575
|
-
`OPENCODE_DISPATCH_MAP[{NUMBER}]
|
|
576
|
-
|
|
577
|
-
|
|
790
|
+
`OPENCODE_DISPATCH_MAP[{NUMBER}]` and immediately persist it to the issue:
|
|
791
|
+
|
|
792
|
+
```bash
|
|
793
|
+
ATTEMPT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
794
|
+
--jq '[.[] | select(.body | contains("FORGE:DISPATCH") and contains("\"state\":\"running\""))] | length + 1')
|
|
795
|
+
if [ "${DRY_RUN:-false}" = "false" ]; then
|
|
796
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DISPATCH -->
|
|
797
|
+
\`\`\`json
|
|
798
|
+
{\"runtime\":\"opencode\",\"child_session_id\":\"{TASK_ID}\",\"attempt\":${ATTEMPT},\"state\":\"running\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}
|
|
799
|
+
\`\`\`"
|
|
800
|
+
fi
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
`background=true` is required for streaming DAG behavior; a foreground task
|
|
804
|
+
makes the orchestrator wait for that issue and reintroduces a wave barrier. The ForgeDock OpenCode plugin opts into
|
|
578
805
|
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` by default. An explicit
|
|
579
806
|
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is a supported degraded mode,
|
|
580
807
|
but it must be reported as non-streaming rather than treated as equivalent to
|
|
@@ -667,6 +894,18 @@ if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
|
667
894
|
if [ "${#DEFERRED_CONCURRENCY_ISSUES[@]}" -gt 0 ]; then
|
|
668
895
|
echo "CONCURRENCY DEFER: ${#DEFERRED_CONCURRENCY_ISSUES[@]} ready issue(s) held back — ${ACTIVE_DISPATCH_COUNT}/${MAX_CONCURRENT} already in flight. Will dispatch as slots free up: ${DEFERRED_CONCURRENCY_ISSUES[*]}"
|
|
669
896
|
fi
|
|
897
|
+
# Filter against the durable board once while forming this engine batch. The tool-call
|
|
898
|
+
# instruction below re-checks immediately before each launch to close the remaining TOCTOU gap.
|
|
899
|
+
CLAIM_SAFE_DISPATCH=()
|
|
900
|
+
for NUM in "${DISPATCH_NOW[@]}"; do
|
|
901
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && claim_conflicts_with_live_holder "$NUM"; then
|
|
902
|
+
DEFERRED_CLAIM_ISSUES+=("$NUM")
|
|
903
|
+
continue
|
|
904
|
+
fi
|
|
905
|
+
CLAIM_SAFE_DISPATCH+=("$NUM")
|
|
906
|
+
done
|
|
907
|
+
DISPATCH_NOW=("${CLAIM_SAFE_DISPATCH[@]}")
|
|
908
|
+
|
|
670
909
|
echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message via forgedock run-issue (headroom was ${HEADROOM})"
|
|
671
910
|
|
|
672
911
|
for NUM in "${DISPATCH_NOW[@]}"; do
|
|
@@ -703,10 +942,10 @@ fi
|
|
|
703
942
|
fi
|
|
704
943
|
```
|
|
705
944
|
|
|
706
|
-
**Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Issue one `Bash(...)` call per issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
|
|
945
|
+
**Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Immediately before each call, run `claim_conflicts_with_live_holder "{NUM}"`; if it returns success, defer that issue rather than dispatching it. Issue one `Bash(...)` call per remaining issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
|
|
707
946
|
|
|
708
947
|
```
|
|
709
|
-
Bash(command="forgedock run-issue {NUM} --lane {PR_BASE}", run_in_background=true, description="Engine-drive issue #{NUM}")
|
|
948
|
+
Bash(command="FORGE_GIST_CAPABLE=${FORGE_GIST_CAPABLE} forgedock run-issue {NUM} --lane {PR_BASE}", run_in_background=true, description="Engine-drive issue #{NUM}")
|
|
710
949
|
```
|
|
711
950
|
|
|
712
951
|
Capture the task id each call returns into `ENGINE_DISPATCH_MAP[{NUM}]` (declared alongside `AGENT_ISSUE_MAP` below — Step 4B's completion handler uses this map to identify which issue a backgrounded engine-mode `Bash` completion notification belongs to, the same role `AGENT_ISSUE_MAP` plays for `agent_completed` notifications):
|
|
@@ -723,10 +962,13 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not dispatch any issues this cycl
|
|
|
723
962
|
**Agent-spawn path (Claude fallback when forgedock CLI unavailable)**: When `FORGEDOCK_AVAILABLE=false` and the runtime is not OpenCode, spawn Agent sub-agents per issue using the template below. This preserves engine state via the SubagentStop hook even without the CLI.
|
|
724
963
|
|
|
725
964
|
When the runtime is OpenCode, skip this Claude-only `Agent(...)` path entirely.
|
|
726
|
-
Use the native `task` call above for initial
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
and
|
|
965
|
+
Use the native `task` call above for initial and newly-ready issue dispatches.
|
|
966
|
+
Translate only those OpenCode orchestration dispatches to `task(...)` with
|
|
967
|
+
`subagent_type="general"` and `background=true`. Review and remediation are
|
|
968
|
+
load-bearing child operations and must run foreground. Do not claim task-id
|
|
969
|
+
resume support: after a restart, reconcile the latest `FORGE:DISPATCH` record
|
|
970
|
+
with durable GitHub workflow state, then dispatch a fresh work-on continuation
|
|
971
|
+
only when the issue is still non-terminal.
|
|
730
972
|
|
|
731
973
|
**REMINDER: You MUST use the template below verbatim when on the Agent-spawn fallback path. Only fill in `{VARIABLES}`. Do NOT rewrite the agent prompt. Do NOT write custom implementation instructions. The agent MUST invoke `/work-on` via the Skill tool — this is the HARD RULE from the top of this file.**
|
|
732
974
|
|
|
@@ -748,6 +990,8 @@ Agent(
|
|
|
748
990
|
**Repository**: {GH_REPO}
|
|
749
991
|
**Repo path**: {REPO_PATH}
|
|
750
992
|
|
|
993
|
+
**KNOWLEDGE GIST CAPABILITY**: This orchestration already probed it: `{FORGE_GIST_CAPABLE}`. Before invoking `/work-on`, run `export FORGE_GIST_CAPABLE={FORGE_GIST_CAPABLE}`. Do not re-probe or attempt Gist creation when it is `false`.
|
|
994
|
+
|
|
751
995
|
**YOUR MISSION**: Invoke `/work-on` via the Skill tool and let it run to completion. `/work-on` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — `/work-on` handles everything including issue closure and label updates in its close phase.
|
|
752
996
|
|
|
753
997
|
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: `workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
|
|
@@ -760,7 +1004,9 @@ Agent(
|
|
|
760
1004
|
- For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER} --under-orchestration')` (prefix from forge.yaml → repos.satellites)
|
|
761
1005
|
- The `--under-orchestration` flag tells `/work-on` to post its phase-entry `FORGE:HEARTBEAT` comments (Phases 0/1/3/5) — this orchestrator's Step 4B.5 stall detector depends on those timestamps. A solo `/work-on` run omits the flag and skips those writes entirely (see `commands/work-on.md` → Orchestration Flag).
|
|
762
1006
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
763
|
-
- **
|
|
1007
|
+
- **File-backed GitHub bodies — entity scope plus read-back is mandatory**: Never stage a `gh --body-file` body at a generic shared `/tmp` path, including one made by bare `mktemp`, and never hand-roll a root-level path such as `/tmp_invbody_31076.txt`, which can hang unattended cleanup. Prefer the session scratchpad or a repo-relative scratch directory on Windows: a native Windows `gh` may not resolve Git Bash `/tmp` reliably. Create a filename that contains the target issue/PR number and an agent-unique token, then use `mktemp` for its random suffix. Put one caller-chosen marker such as `<!-- FORGE:BODY-INTEGRITY:${NUMBER}_investigator_${AGENT_TOKEN} -->` in the body. After every `gh issue create|edit` or `gh pr create|comment` using `--body-file`, re-read the target object and assert the exact marker is present; a mismatch is a hard error. Unique names reduce collisions, but only read-back detects a collision that substitutes plausible-looking content from another agent. Do not rely on eyeballing. (forge#2843, forge#2855) <!-- allowlist:check-command-side-effects -->
|
|
1008
|
+
- **GitHub secondary rate limit**: If a GitHub API call returns HTTP 403 with `secondary rate limit` in its response, do NOT retry it or start a polling loop. Stop GitHub content creation for this phase, report the status and response body to the orchestrator in your final result, and wait for a later batch resume. Retrying extends the throttle for every sibling.
|
|
1009
|
+
- **Temp files — ALWAYS use `mktemp`, NEVER hand-roll a path**: You are one of several agents running concurrently on this host and you share its `/tmp` with all of them. Any time you stage content in a temp file before passing it to `gh` (e.g. `--body-file`), create that path with `mktemp` (e.g. `BODY_FILE="$(mktemp)"`). Never write a temp file to a single-segment root path such as `/tmp_invbody_31076.txt`: Claude Code treats removing it as dangerous and requires an explicit approval that bypass mode cannot clear, hanging unattended runs. Also never use a fixed literal such as `/tmp/body.md` or `/tmp/issue.json`: a fixed path collides with another concurrently-running agent and can silently overwrite the content you staged (or you can silently overwrite theirs) before either of you reads it back. Safe: `BODY_FILE="$(mktemp)"`. Unsafe: `/tmp_invbody_31076.txt` (a missing slash that creates a root-level path) and `/tmp/body.md` (a fixed path). `mktemp` costs nothing and prevents both failures. (forge#2198, forge#2855)
|
|
764
1010
|
- **`docker cp` — NEVER write into a bind-mounted shared container**: If the project you're working on runs its dev/test containers with a bind mount to the main (non-worktree) checkout, `docker cp` into that container writes through the mount into the main checkout — not your isolated per-issue worktree. Before running `docker cp` into any container, confirm its mount source is your own worktree, not a shared/main one; if you can't confirm that, don't assume an in-container test run reflects your feature branch. (forge#2198)
|
|
765
1011
|
- NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
|
|
766
1012
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
|
|
@@ -950,15 +1196,28 @@ fi
|
|
|
950
1196
|
echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message (headroom was ${HEADROOM})"
|
|
951
1197
|
```
|
|
952
1198
|
|
|
953
|
-
Spawn `Agent()` for every issue in `DISPATCH_NOW` (not the raw ready set) using the template above. After the batch spawn, increment `ACTIVE_DISPATCH_COUNT` by
|
|
1199
|
+
Immediately before every Claude `Agent()` or OpenCode `task()` call, run `claim_conflicts_with_live_holder "{NUM}"`; when it returns success, append the issue to `DEFERRED_CLAIM_ISSUES[]` and do not make that call. Spawn `Agent()` for every remaining issue in `DISPATCH_NOW` (not the raw ready set) using the template above. After the batch spawn, increment `ACTIVE_DISPATCH_COUNT` by the number actually dispatched — this happens in addition to, not instead of, capturing each returned agent ID into `AGENT_ISSUE_MAP` above.
|
|
954
1200
|
|
|
955
1201
|
If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle — wait for the next `agent_completed` notification, which frees a slot and re-triggers this dispatch computation (Step 4B).
|
|
956
1202
|
|
|
957
1203
|
### Step 4B: Monitor completions and dispatch newly ready issues
|
|
958
1204
|
|
|
959
|
-
You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. In OpenCode, a native background `task` first returns a `state="running"` result and later injects a synthetic `<task id="..." state="completed">` or `state="error"` result into this same parent session. The later result is the completion event; the initial running result is not completion. Both Claude notifications and OpenCode task-result events must trigger the same per-issue handling immediately. **Do NOT use `sleep` loops, wait for the slowest sibling, or poll before processing an event.** When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for a Claude `agent_completed` notification, `ENGINE_DISPATCH_MAP` for a background-Bash notification, or `OPENCODE_DISPATCH_MAP` for an OpenCode task result — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them.
|
|
1205
|
+
You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. In OpenCode, a native background `task` first returns a `state="running"` result and later injects a synthetic `<task id="..." state="completed">` or `state="error"` result into this same parent session. The later result is the completion event; the initial running result is not completion. Both Claude notifications and OpenCode task-result events must trigger the same per-issue handling immediately. **Do NOT use `sleep` loops, wait for the slowest sibling, or poll before processing an event.** When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for a Claude `agent_completed` notification, `ENGINE_DISPATCH_MAP` for a background-Bash notification, or `OPENCODE_DISPATCH_MAP` for an OpenCode task result — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them. For an OpenCode completion, append this terminal record before releasing capacity:
|
|
960
1206
|
|
|
961
|
-
|
|
1207
|
+
```bash
|
|
1208
|
+
if [ "${DRY_RUN:-false}" = "false" ]; then
|
|
1209
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DISPATCH -->
|
|
1210
|
+
\`\`\`json
|
|
1211
|
+
{\"runtime\":\"opencode\",\"child_session_id\":\"{TASK_ID}\",\"attempt\":${ATTEMPT},\"state\":\"{completed|error}\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}
|
|
1212
|
+
\`\`\`"
|
|
1213
|
+
fi
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
**Fallback**: if a runtime ever fails to deliver a background completion notification, read the latest `FORGE:DISPATCH` record and use the documented label-state recovery path, but do not convert the normal OpenCode path to foreground tasks or a wave barrier.
|
|
1217
|
+
|
|
1218
|
+
For an OpenCode completion, retain `OPENCODE_DISPATCH_MAP[{NUMBER}]` until the terminal `FORGE:DISPATCH` record above has been posted successfully. Then remove that map entry and release capacity. This makes a restart deterministic: a latest `running` record restores the task-to-issue association, while a latest `completed` or `error` record cannot release the same slot twice.
|
|
1219
|
+
|
|
1220
|
+
**Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, a background-Bash completion, or an OpenCode `task` result with `state="completed"`/`state="error"` — decrement `ACTIVE_DISPATCH_COUNT` by 1 — a worker slot has just freed, regardless of what terminal state the issue ended up in. Do this before any stall-recovery/resume logic below. If that agent is then resumed or fallback-dispatched (item 2 below, or Step 4B.5's stall recovery) because it stalled mid-pipeline rather than truly finishing, re-increment `ACTIVE_DISPATCH_COUNT` when the `Agent(resume=...)` or a fresh OpenCode `task(background=true)` continuation is issued — either re-occupies a worker slot exactly like a fresh dispatch does. (Step 4B.5's TIME-BASED hang detector — an agent that never completes at all, as opposed to one that completes at `workflow:engine-error` — remains resume-specific and out of scope for this fix: an engine-dispatched issue that silently hangs still surfaces only via the standard stall-detection alert, and `forgedock resume-stalled` remains available as a separate manual/scripted recovery path for that case. The completion-triggered case — an engine-dispatched issue that DOES complete at `workflow:engine-error` with empty committed state — is still auto-fallen-back by item 2b below, fixed forge#2743.)
|
|
962
1221
|
|
|
963
1222
|
**Ordering requirement (MANDATORY — prevents transient over-cap)**: For every agent that completed in this notification batch, finish its terminal-state check and, if applicable, its resume re-increment (items 1-2 below) BEFORE computing `dispatch_headroom` for item 5's newly-ready-issue dispatch. Computing headroom before a to-be-resumed agent's re-increment would let a genuinely-still-running agent's freed slot be double-booked — once by a fresh dispatch, once by the resume itself — transiently exceeding `MAX_CONCURRENT`. Process every completed agent's items 1-2 to a decision (terminal vs. resumed) first; only then compute headroom once for the batch's item 5 dispatch.
|
|
964
1223
|
|
|
@@ -1019,7 +1278,7 @@ classify_predecessor_state() {
|
|
|
1019
1278
|
|
|
1020
1279
|
A GATED predecessor whose PR later merges reclassifies to DONE the next time `classify_predecessor_state` runs (its label flips to `workflow:merged`) — this is exactly what the merge-triggered wake check (item 6.6 below) relies on.
|
|
1021
1280
|
|
|
1022
|
-
**File-overlap edge re-verification** <!-- Added: forge#1904 --> — `classify_predecessor_state()` answers "is the predecessor resolved enough to proceed," but it says nothing about whether a specific Layer 1/2/3 file-overlap edge (`EDGE_KIND`/`EDGE_FILES`, tagged in `phase-3-dependency.md` Step 3C/3D) was ever real. Both were built from a **pre-build guess** — the predecessor's `FORGE:INVESTIGATOR` "Affected Files" list, or a raw issue-body parse if no investigation existed yet. Once a predecessor reaches FAILED
|
|
1281
|
+
**File-overlap edge re-verification** <!-- Added: forge#1904 --> — `classify_predecessor_state()` answers "is the predecessor resolved enough to proceed," but it says nothing about whether a specific Layer 1/2/3 file-overlap edge (`EDGE_KIND`/`EDGE_FILES`, tagged in `phase-3-dependency.md` Step 3C/3D) was ever real. Both were built from a **pre-build guess** — the predecessor's `FORGE:INVESTIGATOR` "Affected Files" list, or a raw issue-body parse if no investigation existed yet. Once a predecessor reaches FAILED, GATED, **or DONE**, that guess must be checked against ground truth (the predecessor's actual PR diff, or the absence of any PR) before the edge is allowed to keep blocking a dependent or to inject a same-file brief. Without this, a predecessor that reaches `needs-human` or `workflow:invalid` having never touched the guessed shared file (or never opened a PR at all) leaves its dependents gated/skipped on a conflict that never existed — and on the happy path, an over-serialized chain is never reconciled at all and stays serialized for its entire life (forge#2848).
|
|
1023
1282
|
|
|
1024
1283
|
```bash
|
|
1025
1284
|
verify_file_overlap_edge() {
|
|
@@ -1049,9 +1308,25 @@ verify_file_overlap_edge() {
|
|
|
1049
1308
|
# a MERGED PR would already classify the predecessor DONE, not FAILED/GATED — so this filter
|
|
1050
1309
|
# also guarantees `gh pr diff` below targets a live, still-open branch rather than an
|
|
1051
1310
|
# abandoned PR whose branch may since have been force-deleted (e.g. by /cleanup).
|
|
1311
|
+
#
|
|
1312
|
+
# forge#2848 — the DONE call site widens what this can surface: for a DONE predecessor
|
|
1313
|
+
# the live PR is MERGED, not OPEN. That is the *better* case, not a problem — a merged
|
|
1314
|
+
# PR's diff is ground truth rather than a still-moving branch, and `gh pr diff` serves it
|
|
1315
|
+
# from the API, so it keeps working after the head branch is deleted on merge. The
|
|
1316
|
+
# CLOSED-unmerged exclusion is unchanged and still correct for all three call sites.
|
|
1052
1317
|
local PRED_PR
|
|
1318
|
+
local PRED_PR_EXIT
|
|
1053
1319
|
PRED_PR=$(gh pr list -R {GH_REPO} --state all --search "\"Closes #${PRED}\" in:body" \
|
|
1054
|
-
--json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null
|
|
1320
|
+
--json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null)
|
|
1321
|
+
PRED_PR_EXIT=$?
|
|
1322
|
+
|
|
1323
|
+
if [ "$PRED_PR_EXIT" -ne 0 ]; then
|
|
1324
|
+
# Fail-safe: could not determine whether a live predecessor PR exists (transient API
|
|
1325
|
+
# error, rate limit, or search-index lag). Keep the edge rather than conflating a failed
|
|
1326
|
+
# lookup with confirmed absence of a PR and dropping a real conflict.
|
|
1327
|
+
echo "KEEP"
|
|
1328
|
+
return
|
|
1329
|
+
fi
|
|
1055
1330
|
|
|
1056
1331
|
if [ -z "$PRED_PR" ]; then
|
|
1057
1332
|
# Predecessor reached a terminal/gated state having never opened a PR (or only ever had
|
|
@@ -1101,16 +1376,28 @@ verify_file_overlap_edge() {
|
|
|
1101
1376
|
}
|
|
1102
1377
|
```
|
|
1103
1378
|
|
|
1104
|
-
**Call sites**: item 6 (FAILED handling, below)
|
|
1379
|
+
**Call sites**: item 6 (FAILED handling, below), item 6.5 (GATED handling, below), and the DONE arm of the core streaming dispatch loop (below — see "DONE-path edge re-verification") all call this helper — once per `EDGE_KIND`-tagged predecessor edge — before cascading a skip or tracking `blocked-on-human-merge`. `phase-3-dependency.md`'s wake/compaction reconstruction block calls the identical logic (mirrored verbatim, not re-derived) for the case where the predecessor resolves after the orchestrator session has already ended — see that file's "Orchestrator state reconstruction on wake / after compaction" section. This mirroring is deliberate: keeping the check in exactly one function definition, called (not reimplemented) from both files, avoids the same drift class forge#1812 had to fix once already for DONE/GATED/FAILED classification itself.
|
|
1105
1380
|
|
|
1106
1381
|
**Recursive cascade note (item 6 only)**: when a dependent `DEP` is itself marked "skipped — dependency failed" (edge confirmed `KEEP`, not dropped), `DEP` becomes the new FAILED anchor for its own direct dependents — re-run this same `verify_file_overlap_edge` check at that next hop too, rather than assuming every transitive descendant is unconditionally skipped. A grandchild dependent whose only path to the failure runs through a spurious (droppable) edge one hop down must not be skipped just because its parent was.
|
|
1107
1382
|
|
|
1108
|
-
**Never called from**: the
|
|
1383
|
+
**Never called from**: the `IN_PROGRESS` branch (nothing to re-verify yet — the predecessor hasn't concluded).
|
|
1384
|
+
|
|
1385
|
+
**DONE-path edge re-verification** <!-- Added: forge#2848 --> — the DONE branch *does* call this helper, and it is the only call site that fires on the happy path. Be precise about what it buys, because the obvious reading is wrong:
|
|
1386
|
+
|
|
1387
|
+
**It does NOT release the dependent.** A DONE predecessor already satisfies its own edge — see the `;; # satisfied` arm and the `ALL_PREDS_DONE` gate below. Dropping an edge whose predecessor is already DONE releases nobody, so re-verification alone cannot re-parallelize anything. What it actually buys is two things:
|
|
1388
|
+
|
|
1389
|
+
1. **Suppressing a spurious same-file brief.** The DONE arm spends up to three `gh api` comment fetches per edge and then injects a `SAME_FILE_BRIEF` excerpt into the dependent's dispatch context. When the predecessor's real diff never touched the guessed file, that brief is not merely wasted tokens — it actively misleads the agent into coordinating on a file the predecessor never wrote. Verifying **before** those fetches skips all three calls and the brief.
|
|
1390
|
+
2. **Producing the disproof signal that enables re-derivation.** This is the part that actually re-parallelizes. A `DROP` on a `body-fallback`-provenance edge is evidence that the whole cohort's extraction is suspect: the first real diff has just contradicted the guess the chain was built on. By that point the still-blocked descendants have been investigated and possibly contracted, so re-running Layer 1 extraction against those now-higher-provenance sources and dropping edges that no longer hold is what collapses the chain. This is the step an operator performed by hand in the batch that motivated forge#2848; wiring it here makes it automatic.
|
|
1109
1391
|
|
|
1110
1392
|
**Core streaming dispatch loop**: After processing each agent completion, check the DAG for newly unblocked issues. If any issue now has all predecessors classified `DONE`, dispatch it immediately (run Steps 4A.pre.0, 4A.pre, and 4A for the newly ready issues). This is the key difference from the wave model — issues dispatch as soon as their specific predecessors complete, not after an entire group finishes.
|
|
1111
1393
|
|
|
1112
1394
|
```bash
|
|
1113
1395
|
# After each agent completion, check for newly ready issues:
|
|
1396
|
+
READINESS_RESCAN=true
|
|
1397
|
+
while [ "$READINESS_RESCAN" = "true" ]; do
|
|
1398
|
+
# Re-derivation can remove an unresolved predecessor from an issue that this
|
|
1399
|
+
# pass has already visited. Repeat the scan so it can dispatch this cycle.
|
|
1400
|
+
READINESS_RESCAN=false
|
|
1114
1401
|
for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
1115
1402
|
ALL_PREDS_DONE=true
|
|
1116
1403
|
ANY_PRED_GATED=false
|
|
@@ -1128,6 +1415,100 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
|
1128
1415
|
EDGE_TYPE="${EDGE_KIND["${PRED}:${BLOCKED_NUM}"]:-}"
|
|
1129
1416
|
if [ -n "$EDGE_TYPE" ] && [ -z "${EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]:-}" ]; then
|
|
1130
1417
|
EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]=1
|
|
1418
|
+
|
|
1419
|
+
# DONE-path edge re-verification (forge#2848). Runs FIRST — before the three
|
|
1420
|
+
# `gh api` brief fetches below — so a disproven edge costs zero extra API calls
|
|
1421
|
+
# and injects no brief. EDGE_BRIEFED is already set above, so a DROP is not
|
|
1422
|
+
# retried on the next completion cycle.
|
|
1423
|
+
#
|
|
1424
|
+
# Fail-safe direction is unchanged: verify_file_overlap_edge returns KEEP on any
|
|
1425
|
+
# fetch failure, so a transient API error can only ever preserve the brief, never
|
|
1426
|
+
# silently suppress a real one.
|
|
1427
|
+
EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$BLOCKED_NUM")
|
|
1428
|
+
if [ "$EDGE_VERDICT" = "DROP" ]; then
|
|
1429
|
+
echo "DONE-path re-verification: edge #${PRED} → #${BLOCKED_NUM} (${EDGE_TYPE}, files: ${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-?}) DROPPED — #${PRED}'s merged diff never touched the guessed file. Skipping same-file brief."
|
|
1430
|
+
|
|
1431
|
+
# Leave SAME_FILE_BRIEF[$BLOCKED_NUM] UNSET rather than setting it empty —
|
|
1432
|
+
# the dispatch-context builder treats unset as the no-op case, and an empty
|
|
1433
|
+
# string would render an orphaned "shared file context" heading with nothing
|
|
1434
|
+
# under it.
|
|
1435
|
+
|
|
1436
|
+
# Cohort re-derivation (forge#2848) — the step that actually releases
|
|
1437
|
+
# dependents. Gated on `body-fallback` provenance only: a DROP against a
|
|
1438
|
+
# contract- or investigation-derived list is a one-off miss, but a DROP
|
|
1439
|
+
# against a raw-issue-body scrape is evidence the whole cohort's extraction
|
|
1440
|
+
# is suspect (see phase-3-dependency.md Layer 4's cohort-confidence guidance).
|
|
1441
|
+
if [ "${FILE_SOURCE[$PRED]:-}" = "body-fallback" ] && [ -n "$AFFECTED_FILES_SCRIPT" ]; then
|
|
1442
|
+
# Re-derive only direct descendants of this DONE predecessor that
|
|
1443
|
+
# are still blocked now. This intentionally excludes unrelated
|
|
1444
|
+
# blocked issues and descendants whose PRED edge was already removed.
|
|
1445
|
+
STILL_BLOCKED_DESCENDANTS=()
|
|
1446
|
+
for CANDIDATE in {all_blocked_issue_numbers}; do
|
|
1447
|
+
case " ${PREDECESSORS[$CANDIDATE]:-} " in
|
|
1448
|
+
*" $PRED "*) STILL_BLOCKED_DESCENDANTS+=("$CANDIDATE") ;;
|
|
1449
|
+
esac
|
|
1450
|
+
done
|
|
1451
|
+
for DESC in "${STILL_BLOCKED_DESCENDANTS[@]}"; do
|
|
1452
|
+
# Memoize conclusive results per descendant per batch. An inconclusive
|
|
1453
|
+
# extraction gets one retry so a transient failure or pre-contract
|
|
1454
|
+
# attempt does not permanently foreclose re-derivation. The two-attempt
|
|
1455
|
+
# cap preserves an O(descendants) API budget for the hot dispatch loop.
|
|
1456
|
+
[ -n "${EDGE_REDERIVED[$DESC]:-}" ] && continue
|
|
1457
|
+
REDERIVE_ATTEMPTS=${EDGE_REDERIVE_ATTEMPTS[$DESC]:-0}
|
|
1458
|
+
[ "$REDERIVE_ATTEMPTS" -ge 2 ] && continue
|
|
1459
|
+
EDGE_REDERIVE_ATTEMPTS[$DESC]=$((REDERIVE_ATTEMPTS + 1))
|
|
1460
|
+
|
|
1461
|
+
# Re-run Layer 1 extraction. By now DESC has been investigated and
|
|
1462
|
+
# possibly contracted, so this typically returns a higher-provenance
|
|
1463
|
+
# list than the body scrape the original edge was built from. Resolve
|
|
1464
|
+
# the helper through the runtime path, never a bare `bash scripts/...`
|
|
1465
|
+
# (#2794) — same resolver precedence as phase-3-dependency.md Layer 1.
|
|
1466
|
+
REDERIVE_OUT=$(bash "$AFFECTED_FILES_SCRIPT" "$DESC" -R {GH_REPO})
|
|
1467
|
+
REDERIVE_PROV=$(echo "$REDERIVE_OUT" | head -1 | sed 's/^PROVENANCE=//')
|
|
1468
|
+
REDERIVE_FILES=$(echo "$REDERIVE_OUT" | tail -n +2)
|
|
1469
|
+
|
|
1470
|
+
# Fail-safe: only act on a CONFIRMED-empty overlap. `error` means the
|
|
1471
|
+
# extraction was inconclusive (a `gh` call failed — forge#2504), and
|
|
1472
|
+
# `none`/`body-fallback` mean we learned nothing better than what we
|
|
1473
|
+
# already had. In all three cases keep every edge untouched.
|
|
1474
|
+
case "$REDERIVE_PROV" in
|
|
1475
|
+
contract-deliverables|affected-files-section) EDGE_REDERIVED[$DESC]=1 ;;
|
|
1476
|
+
*) echo " re-derivation for #${DESC} inconclusive (provenance: ${REDERIVE_PROV}) — keeping all edges"; continue ;;
|
|
1477
|
+
esac
|
|
1478
|
+
|
|
1479
|
+
# Re-derive only PRED's edge into DESC. PRED is the DONE predecessor
|
|
1480
|
+
# whose merged diff just disproved this edge; other predecessors may
|
|
1481
|
+
# still be building and must retain their serialization edges.
|
|
1482
|
+
for DESC_PRED in "$PRED"; do
|
|
1483
|
+
[ -z "${EDGE_KIND["${DESC_PRED}:${DESC}"]:-}" ] && continue
|
|
1484
|
+
STILL_OVERLAPS=false
|
|
1485
|
+
for EF in ${EDGE_FILES["${DESC_PRED}:${DESC}"]:-}; do
|
|
1486
|
+
[ -z "$EF" ] && continue
|
|
1487
|
+
EF_CLEAN="${EF%/}"
|
|
1488
|
+
EF_ESCAPED=$(printf '%s' "$EF_CLEAN" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
1489
|
+
if echo "$REDERIVE_FILES" | grep -qE "(^|/)${EF_ESCAPED}(/|$)"; then
|
|
1490
|
+
STILL_OVERLAPS=true
|
|
1491
|
+
break
|
|
1492
|
+
fi
|
|
1493
|
+
done
|
|
1494
|
+
if [ "$STILL_OVERLAPS" = "false" ]; then
|
|
1495
|
+
echo " re-derived edge #${DESC_PRED} → #${DESC}: DROPPED — '${EDGE_FILES["${DESC_PRED}:${DESC}"]}' absent from #${DESC}'s refreshed ${REDERIVE_PROV} file list"
|
|
1496
|
+
unset 'EDGE_KIND['"${DESC_PRED}:${DESC}"']'
|
|
1497
|
+
unset 'EDGE_FILES['"${DESC_PRED}:${DESC}"']'
|
|
1498
|
+
# Remove DESC_PRED from PREDECESSORS[$DESC] so the ALL_PREDS_DONE
|
|
1499
|
+
# gate below stops waiting on it. DESC becomes dispatch-eligible on
|
|
1500
|
+
# this same cycle if that was its last outstanding predecessor.
|
|
1501
|
+
PREDECESSORS[$DESC]=$(echo "${PREDECESSORS[$DESC]}" | tr ' ' '\n' | grep -vx "$DESC_PRED" | tr '\n' ' ')
|
|
1502
|
+
READINESS_RESCAN=true
|
|
1503
|
+
fi
|
|
1504
|
+
done
|
|
1505
|
+
done
|
|
1506
|
+
fi
|
|
1507
|
+
|
|
1508
|
+
# Edge disproven — nothing further to do for this (PRED, BLOCKED_NUM) pair.
|
|
1509
|
+
continue
|
|
1510
|
+
fi
|
|
1511
|
+
|
|
1131
1512
|
SHARED_FILE="${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-}"
|
|
1132
1513
|
|
|
1133
1514
|
# Prefer FORGE:BUILDER (the concrete "what changed" record). Fall back to
|
|
@@ -1179,6 +1560,7 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
|
1179
1560
|
# Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
|
|
1180
1561
|
fi
|
|
1181
1562
|
done
|
|
1563
|
+
done
|
|
1182
1564
|
# Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues. Step 4A's own dispatch-batch
|
|
1183
1565
|
# computation (the HEADROOM/DISPATCH_NOW logic, forge#1912) is what actually caps this —
|
|
1184
1566
|
# "newly ready" here just means "eligible," not "guaranteed to dispatch this cycle." Any
|
|
@@ -1338,11 +1720,7 @@ done
|
|
|
1338
1720
|
# After CLAIM_RELEASED for issue NUM:
|
|
1339
1721
|
# Read all active FORGE:CLAIM annotations from coordination issue
|
|
1340
1722
|
if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
|
|
1341
|
-
|
|
1342
|
-
--jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
|
|
1343
|
-
select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
|
|
1344
|
-
map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
|
|
1345
|
-
files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
|
|
1723
|
+
ACTIVE_CLAIMS=$(read_active_claims "$COORD_NUM" 2>/dev/null || echo '[]')
|
|
1346
1724
|
# For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
|
|
1347
1725
|
# is disjoint from all remaining active claims. If so, mark it ready.
|
|
1348
1726
|
# (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
|
|
@@ -1406,7 +1784,7 @@ done
|
|
|
1406
1784
|
run_in_background=true,
|
|
1407
1785
|
prompt="You are remediating GitHub PR #{GATING_PR} for the {PROJECT_NAME} project (repo: {GH_REPO}), which is currently held at `needs-human` on its linked issue #{PRED}.
|
|
1408
1786
|
|
|
1409
|
-
**YOUR MISSION**: Invoke `Skill(skill='work-on', args='{GATING_PR} --remediate --issue {PRED} --repo {GH_REPO} --gh-flag {GH_FLAG}')` and let it run to completion. This is a self-contained flow: it checks out the PR branch, fixes any fixable review findings, re-reviews, and either auto-lands the PR, holds it at `workflow:awaiting-merge` for a human, re-escalates back to `needs-human`, or reports the block as policy-level and unfixable. Do NOT intervene manually — do not run raw git/gh commands yourself.
|
|
1787
|
+
**YOUR MISSION**: Invoke `Skill(skill='work-on', args='{GATING_PR} --remediate --issue {PRED} --repo {GH_REPO} --gh-flag {GH_FLAG}')` and let it run to completion. This is a self-contained flow: it classifies the gate first, replaces `needs-human` with `workflow:in-review` only for fixable remediation, checks out the PR branch, fixes any fixable review findings, re-reviews, and either auto-lands the PR, holds it at `workflow:awaiting-merge` for a human, re-escalates back to `needs-human`, or reports the block as policy-level and unfixable. Do NOT intervene manually — do not run raw git/gh commands yourself.
|
|
1410
1788
|
|
|
1411
1789
|
**DO NOT STOP EARLY**: if the Skill call returns without a terminal `REMEDIATE_RESULT.status`, invoke it again — it re-reads GitHub state and resumes. Terminal statuses are: `COMPLETE`, `ALREADY_DONE`, `UNFIXABLE`, `BLOCKED`.
|
|
1412
1790
|
|
|
@@ -1552,6 +1930,7 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
1552
1930
|
```
|
|
1553
1931
|
✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
|
|
1554
1932
|
✗ #{NUMBER} — {title} → {reason for failure}
|
|
1933
|
+
⚠ #{NUMBER} — {title} → PR #{PR} blocked: review panel degraded (full fresh-session re-review required)
|
|
1555
1934
|
⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
|
|
1556
1935
|
⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved, `main`/deploy-gate base — human merge only, no diagnosis needed; non-`main` bases now auto-land to `workflow:merged` via remediate.md M7, forge#2570)
|
|
1557
1936
|
🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
|
|
@@ -1577,6 +1956,8 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
1577
1956
|
dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
|
|
1578
1957
|
"Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
|
|
1579
1958
|
|
|
1959
|
+
Before printing a terminal status for a PR, inspect its labels. If it has `review-degraded`, print the dedicated blocked line above and retain the issue's `needs-human` state. Never report a degraded review as merged or awaiting-merge.
|
|
1960
|
+
|
|
1580
1961
|
9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
|
|
1581
1962
|
|
|
1582
1963
|
**Termination condition**: All issues in the DAG have reached `DONE` or `FAILED` (merged, invalid, or skipped due to dependency failure) — OR are `blocked-on-human-merge` (item 6.5) with no further dispatchable work remaining in the batch. These two outcomes are reported differently: a batch where every issue is `DONE`/`FAILED` is a **clean drain**; a batch where one or more issues remain `blocked-on-human-merge` is a **paused drain** — the active dispatch loop stops (there is nothing left to do until a human merges a gating PR) but this MUST be reported as paused, not as fully complete (see `phase-6-report.md`'s `🔗 Blocked-on-Merge` section). `needs-human` predecessors with no open PR are neither — they remain GATED indefinitely until either a PR appears (dependent moves to blocked-on-human-merge) or the predecessor itself resolves; do not treat isolated `needs-human` issues with no dependents as blocking termination. When either drain condition is met, check whether deferred review-spawned findings exist (accumulated in `DEFERRED_FINDINGS` during Step 4C). If deferred findings exist → proceed to Step 4F (Completion Sweep). If no deferred findings → **call `release_orchestrator_lease()` (Step 4A-pre.-1)** — this is a clean/paused drain of this session's own dispatch loop, so the lease should not be left held for a now-idle session — then proceed to Phase 5. <!-- Added: forge#2627 -->
|
|
@@ -1737,6 +2118,56 @@ that mode only exists as the explicit, one-shot `--include-backlog` opt-in at Ph
|
|
|
1737
2118
|
never silently widen to the full backlog regardless of any flag, since nothing here is a
|
|
1738
2119
|
human-in-the-loop request. <!-- Added: forge#2628 -->
|
|
1739
2120
|
|
|
2121
|
+
### Step 4C.0: Full-repository actionable-issue intake (`policy: all` only)
|
|
2122
|
+
|
|
2123
|
+
An operator-authorised `orchestration.cascade.policy: all` run must also check for newly-created
|
|
2124
|
+
open issues that its own agents did not report and that lack `review-finding`. Run this sweep
|
|
2125
|
+
after every five terminal completions, before the next dispatch cycle. It is not a cascade
|
|
2126
|
+
replacement: Step 4C's `BATCH_T0` filter still governs review-finding recursion.
|
|
2127
|
+
|
|
2128
|
+
At batch start, seed `SEEN_OPEN_ISSUES` with the initial resolved issue set. On each sweep, list
|
|
2129
|
+
the complete open set (`gh issue list --state open --limit 500 --json number,title,labels`), diff
|
|
2130
|
+
it against `SEEN_OPEN_ISSUES`, then mark every returned number seen whether or not it is admitted.
|
|
2131
|
+
For each newly-seen issue, add it to the normal Phase 3 DAG intake only when it has no terminal or
|
|
2132
|
+
in-progress workflow label, `needs-human`, or `workflow:decomposed`; preserve its labels and
|
|
2133
|
+
record why every excluded issue was skipped. Do not filter on `review-finding`, author, creation
|
|
2134
|
+
time, or agent trajectory. This is how CI-created issues enter an all-policy run without silently
|
|
2135
|
+
adopting pre-existing work in other policies.
|
|
2136
|
+
|
|
2137
|
+
```bash
|
|
2138
|
+
if [ "$CASCADE_POLICY_NAME" = "all" ] && [ "$((TERMINAL_COMPLETIONS % 5))" -eq 0 ] && \
|
|
2139
|
+
[ "$TERMINAL_COMPLETIONS" -gt "$FULL_REPO_SWEEP_COUNT" ]; then
|
|
2140
|
+
FULL_REPO_SWEEP_COUNT=$TERMINAL_COMPLETIONS
|
|
2141
|
+
FULL_REPO_OPEN=$(gh issue list -R {GH_REPO} --state open --limit 500 --json number,title,labels)
|
|
2142
|
+
while IFS= read -r ISSUE; do
|
|
2143
|
+
NUM=$(echo "$ISSUE" | jq -r '.number')
|
|
2144
|
+
[ -n "${SEEN_OPEN_ISSUES[$NUM]:-}" ] && continue
|
|
2145
|
+
SEEN_OPEN_ISSUES[$NUM]=1
|
|
2146
|
+
LABELS=$(echo "$ISSUE" | jq -r '[.labels[].name] | join(",")')
|
|
2147
|
+
if echo "$LABELS" | grep -qE '(^|,)(workflow:(merged|invalid|awaiting-merge|building|in-review|decomposed)|needs-human)(,|$)'; then
|
|
2148
|
+
echo "Full-repo intake: #${NUM} seen but not admitted (terminal, in-progress, or human-gated)"
|
|
2149
|
+
continue
|
|
2150
|
+
fi
|
|
2151
|
+
FULL_REPO_INTAKE+=("$NUM")
|
|
2152
|
+
echo "Full-repo intake: admitted newly-seen actionable issue #${NUM}"
|
|
2153
|
+
done < <(echo "$FULL_REPO_OPEN" | jq -c '.[]')
|
|
2154
|
+
# Feed FULL_REPO_INTAKE through the existing Phase 3 extraction/DAG/claims gates.
|
|
2155
|
+
fi
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
### Step 4C.1: Mechanical automated-alert deduplication (`policy: all` opt-in)
|
|
2159
|
+
|
|
2160
|
+
`orchestration.cascade.dedup_automated: true` permits a narrow exception to the normal
|
|
2161
|
+
no-dedup rule for newly-seen automated alerts. Select one lowest-numbered canonical issue and
|
|
2162
|
+
close another issue only when all of these are true: both authors are bot/app accounts; normalized
|
|
2163
|
+
titles are identical; the generator identity is identical; the triggering condition is identical;
|
|
2164
|
+
and the closing comment links the retained canonical issue. Never apply title similarity to a
|
|
2165
|
+
human-authored report, or when generator/trigger provenance is absent or differs. The reference
|
|
2166
|
+
predicate is `canDeduplicateAutomatedAlert()` in `bin/engine/admission.mjs`.
|
|
2167
|
+
|
|
2168
|
+
This opt-in handles mechanically identical alerts, not substantive bug adjudication. All other
|
|
2169
|
+
possible duplicates remain separate and go through investigation.
|
|
2170
|
+
|
|
1740
2171
|
```bash
|
|
1741
2172
|
# Method 1: Read TRAJECTORY comments from completed issues for "Finding issues" row
|
|
1742
2173
|
for NUM in {completed_issue_number}; do
|
|
@@ -1757,13 +2188,37 @@ gh issue list -R {GH_REPO} --state open --search "label:review-finding created:>
|
|
|
1757
2188
|
|
|
1758
2189
|
**If review-finding issues were spawned:**
|
|
1759
2190
|
|
|
2191
|
+
**Amplification accounting (MANDATORY, before cascade admission):** Count every finding created by this batch, including findings later deferred by any cascade rule. After each merged unit, compute `FINDINGS_SPAWNED / MERGED_UNITS`, append the value to `AMPLIFICATION_RATIO_HISTORY`, and print a convergence warning when the latest `CONVERGENCE_WINDOW` observations are all `>= 1.0`. A high ratio is a signal, not a failure: valuable deep review findings can legitimately raise it.
|
|
2192
|
+
|
|
2193
|
+
For each finding, extract its source PR from `**Source**: PR #N` and increment `FINDINGS_BY_SOURCE_PR[N]`. Classify it as a **same-lineage refinement** only when that source PR closes a `review-finding` issue and both that parent finding and this finding name the same affected file; otherwise classify it as **new surface**. Record the finding number in `REFINEMENT_FINDINGS` or `NEW_SURFACE_FINDINGS` respectively. If provenance or a file path cannot be established, classify as new surface conservatively; never suppress it as a refinement.
|
|
2194
|
+
|
|
2195
|
+
When `CASCADE_MAX_AMPLIFICATION` is not `off`, and the current ratio is greater than that ceiling, defer only newly discovered entries in `REFINEMENT_FINDINGS` with reason `amplification bound exceeded (same-lineage refinement)`. Add them to `DEFERRED_FINDINGS` and `DEFERRED_REASONS` so Step 4F re-evaluates them after the batch drains. Do not apply this bound to new-surface findings, P1/P2 findings, or any finding when the option is `off`.
|
|
2196
|
+
|
|
2197
|
+
```bash
|
|
2198
|
+
# Run once for each completed issue before collecting its findings. Only merged
|
|
2199
|
+
# units advance the denominator; invalid/skipped units do not imply delivered work.
|
|
2200
|
+
if gh issue view "$NUM" -R {GH_REPO} --json labels \
|
|
2201
|
+
--jq '[.labels[].name | select(. == "workflow:merged")] | length' | grep -qx '1'; then
|
|
2202
|
+
MERGED_UNITS=$((MERGED_UNITS + 1))
|
|
2203
|
+
AMPLIFICATION_RATIO=$(awk "BEGIN { printf \"%.2f\", $FINDINGS_SPAWNED / $MERGED_UNITS }")
|
|
2204
|
+
AMPLIFICATION_RATIO_HISTORY+=("$AMPLIFICATION_RATIO")
|
|
2205
|
+
if [ "${#AMPLIFICATION_RATIO_HISTORY[@]}" -ge "$CONVERGENCE_WINDOW" ]; then
|
|
2206
|
+
RECENT_RATIOS=("${AMPLIFICATION_RATIO_HISTORY[@]: -$CONVERGENCE_WINDOW}")
|
|
2207
|
+
if printf '%s\n' "${RECENT_RATIOS[@]}" | awk '$1 < 1 { exit 1 }'; then
|
|
2208
|
+
echo "CONVERGENCE WARNING: amplification has remained >= 1.0 for ${CONVERGENCE_WINDOW} merged units (${AMPLIFICATION_RATIO} current). This can be productive review refinement, but the batch is not shrinking."
|
|
2209
|
+
fi
|
|
2210
|
+
fi
|
|
2211
|
+
fi
|
|
2212
|
+
|
|
2213
|
+
```
|
|
2214
|
+
|
|
1760
2215
|
**Cascade control (MANDATORY — run before folding findings into the DAG):**
|
|
1761
2216
|
|
|
1762
2217
|
For each spawned finding, determine whether it should be **executed** or **deferred**:
|
|
1763
2218
|
|
|
1764
2219
|
**Evaluation order** (first matching rule wins):
|
|
1765
2220
|
0. **Batch fully human-gated** (`BATCH_FULLY_GATED == true`, always defer, even for P1/P2) <!-- Added: forge#1814 -->: The original batch (see Step 4B item 6.7) has exhausted into DONE/FAILED/GATED with nothing left `IN_PROGRESS` — the real blockers are the GATED issues, not a lack of dispatchable findings. Dispatching a new review-finding here cannot produce net batch progress; it only inflates the open-issue count while the productive path waits on a human merge. Always defer, checked before generation and priority. Rationale: this is the idle/backpressure policy this issue adds — without it, rule 2 (below) unconditionally executes P1/P2 findings regardless of how gated the rest of the batch is, which is the root cause of the net-negative churn this policy exists to stop. **Configurable** via `orchestration.cascade.defer_on_batch_gated` (default `true`; `false` under `policy: all` — forge#2234).
|
|
1766
|
-
1. **Generation ≥ 2** (always defer, even for P1/P2, for
|
|
2221
|
+
1. **Generation ≥ 2** (always defer from individual autonomous dispatch, even for P1/P2, for mid-run cascade — see scope note below): Finding was spawned by an issue that was itself a review-finding. A P3 finding at or below the finite `orchestration.cascade.batch_max_generation` ceiling (default 2) is retained only as a candidate for the bounded P3 batch pass; it remains deferred if no batch forms. A formed batch records `FORGE:BATCH_MAX_GENERATION` and its generation-2+ members. No P1/P2, or finding above that ceiling, takes this exception. Rationale: gen-2+ individual cascade is theoretically unbounded — cap it here while permitting one auditable aggregation unit. **Scope**: this rule caps *autonomous* cascade — findings discovered and re-triaged automatically during an unattended run. It is not a cap on what a human can explicitly request. When an operator directly asks for cascade/review-finding work via `phase-1-resolve.md`'s dedicated `cascade`/`review-findings`/`findings` resolution (with `--include-deferred`/`--allow-gen2`, or the `orchestration.cascade.max_generation` config lever from #2234), those findings enter the DAG through Phase 1 resolution, not through this Step 4C mid-run triage. `batch_max_generation` is separate from `max_generation`, and remains finite even under `policy: all`. <!-- Added: forge#2231, forge#2849 -->
|
|
1767
2222
|
2. **Priority override** (P1 or P2 → always execute): If the finding is labeled P1 or P2, skip all remaining heuristics and execute. Rationale: high-priority findings must never be suppressed by keyword matching.
|
|
1768
2223
|
3. **Comment/typo heuristic** (P3 and below only): Finding title contains the word "comment" or "typo" (case-insensitive). These are 1-line cosmetic fixes that do not block other work. **Configurable** via `orchestration.cascade.keyword_heuristic` (default `true`; `false` under `policy: all` — forge#2234).
|
|
1769
2224
|
4. **P3 + same-file overlap**: Finding is labeled `P3` AND the file it targets overlaps with ANY file already in the current batch (active or queued in the DAG). Rationale: same-file P3 findings add predecessor edges that serialize agents — one finding per original issue increases wall-clock time with no proportional value. **Configurable** via `orchestration.cascade.p3_same_file_defer` (default `true`; `false` under `policy: all` — forge#2234).
|
|
@@ -1799,6 +2254,22 @@ ALL_BATCH_FILES=$(echo "$ALL_BATCH_FILES" | sort -u | grep -v '^$')
|
|
|
1799
2254
|
```
|
|
1800
2255
|
|
|
1801
2256
|
```bash
|
|
2257
|
+
# Use the same bounded ancestor walk as Phase 1 so batching records actual depth,
|
|
2258
|
+
# not just whether the immediate source is a review finding.
|
|
2259
|
+
compute_finding_generation() {
|
|
2260
|
+
local body="$1" generation=1 hops=0 source_num source_data
|
|
2261
|
+
while [ "$hops" -lt 10 ]; do
|
|
2262
|
+
source_num=$(echo "$body" | grep -ioE 'spawned from issue #[0-9]+|source issue[: #]+[0-9]+' | head -1 | grep -oE '[0-9]+$')
|
|
2263
|
+
[ -z "$source_num" ] && break
|
|
2264
|
+
source_data=$(gh issue view "$source_num" -R {GH_REPO} --json labels,body 2>/dev/null) || break
|
|
2265
|
+
echo "$source_data" | jq -e '[.labels[].name] | index("review-finding")' >/dev/null 2>&1 || break
|
|
2266
|
+
generation=$((generation + 1))
|
|
2267
|
+
body=$(echo "$source_data" | jq -r '.body')
|
|
2268
|
+
hops=$((hops + 1))
|
|
2269
|
+
done
|
|
2270
|
+
echo "$generation"
|
|
2271
|
+
}
|
|
2272
|
+
|
|
1802
2273
|
# For each finding, check its priority label and generation
|
|
1803
2274
|
# NOTE: DEFERRED_FINDINGS, QUEUED_FINDINGS, and DEFERRED_REASONS are declared at
|
|
1804
2275
|
# batch scope in Step 4A.pre — do NOT re-initialize them here (Step 4C runs per-agent).
|
|
@@ -1806,6 +2277,45 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
1806
2277
|
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,updatedAt \
|
|
1807
2278
|
--jq '{labels: [.labels[].name], title: .title, body: .body, updatedAt: .updatedAt}')
|
|
1808
2279
|
|
|
2280
|
+
# Count each finding once and retain source provenance for the final per-PR
|
|
2281
|
+
# breakdown. Missing source/file evidence fails open as new surface.
|
|
2282
|
+
FINDING_BODY=$(echo "$FINDING_DATA" | jq -r '.body')
|
|
2283
|
+
SOURCE_PR=""
|
|
2284
|
+
if [[ "$FINDING_BODY" =~ \*\*[Ss]ource\*\*:[[:space:]]*[Pp][Rr][[:space:]]*\#([0-9]+) ]]; then
|
|
2285
|
+
SOURCE_PR="${BASH_REMATCH[1]}"
|
|
2286
|
+
fi
|
|
2287
|
+
if [ -z "${AMPLIFICATION_FINDING_SEEN[$FINDING_NUM]:-}" ]; then
|
|
2288
|
+
AMPLIFICATION_FINDING_SEEN[$FINDING_NUM]=1
|
|
2289
|
+
FINDINGS_SPAWNED=$((FINDINGS_SPAWNED + 1))
|
|
2290
|
+
[ -n "$SOURCE_PR" ] && FINDINGS_BY_SOURCE_PR[$SOURCE_PR]=$(( ${FINDINGS_BY_SOURCE_PR[$SOURCE_PR]:-0} + 1 ))
|
|
2291
|
+
fi
|
|
2292
|
+
FINDING_IS_REFINEMENT=false
|
|
2293
|
+
FINDING_FILE_FOR_LINEAGE=$(echo "$FINDING_BODY" | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
|
|
2294
|
+
if [ -n "$SOURCE_PR" ] && [ -n "$FINDING_FILE_FOR_LINEAGE" ]; then
|
|
2295
|
+
SOURCE_ISSUE=""
|
|
2296
|
+
SOURCE_PR_BODY=$(gh pr view "$SOURCE_PR" -R {GH_REPO} --json body --jq '.body' 2>/dev/null || echo "")
|
|
2297
|
+
SOURCE_PR_BODY_LOWER=$(echo "$SOURCE_PR_BODY" | tr '[:upper:]' '[:lower:]')
|
|
2298
|
+
if [[ "$SOURCE_PR_BODY_LOWER" =~ (close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]+\#([0-9]+) ]]; then
|
|
2299
|
+
SOURCE_ISSUE="${BASH_REMATCH[3]}"
|
|
2300
|
+
fi
|
|
2301
|
+
if [ -n "$SOURCE_ISSUE" ]; then
|
|
2302
|
+
PARENT_DATA=$(gh issue view "$SOURCE_ISSUE" -R {GH_REPO} --json labels,body \
|
|
2303
|
+
--jq '{labels: [.labels[].name], body: .body}' 2>/dev/null || echo '{}')
|
|
2304
|
+
PARENT_FILE=$(echo "$PARENT_DATA" | jq -r '.body // ""' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
|
|
2305
|
+
if echo "$PARENT_DATA" | jq -e '[.labels[] | select(. == "review-finding")] | length > 0' >/dev/null && \
|
|
2306
|
+
[ "$PARENT_FILE" = "$FINDING_FILE_FOR_LINEAGE" ]; then
|
|
2307
|
+
FINDING_IS_REFINEMENT=true
|
|
2308
|
+
REFINEMENT_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
|
|
2309
|
+
else
|
|
2310
|
+
NEW_SURFACE_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
|
|
2311
|
+
fi
|
|
2312
|
+
else
|
|
2313
|
+
NEW_SURFACE_FINDINGS[$FINDING_NUM]="$SOURCE_PR"
|
|
2314
|
+
fi
|
|
2315
|
+
else
|
|
2316
|
+
NEW_SURFACE_FINDINGS[$FINDING_NUM]="${SOURCE_PR:-unknown}"
|
|
2317
|
+
fi
|
|
2318
|
+
|
|
1809
2319
|
# Code-branch repair guard (MANDATORY — before priority/defer heuristics below).
|
|
1810
2320
|
# A review-finding with no **Code branch** annotation, whose parent PR's base is
|
|
1811
2321
|
# not the staging fast lane, would otherwise silently fall through to
|
|
@@ -1870,30 +2380,33 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
1870
2380
|
# as its own outcome and fail closed, mirroring the concurrent-edit branch below
|
|
1871
2381
|
# rather than silently falling through to the unprotected write.
|
|
1872
2382
|
echo "REPAIR: #${FINDING_NUM} skipped — freshness re-fetch failed (network error or API rate limit); cannot verify no concurrent edit occurred, failing closed instead of risking a silent clobber"
|
|
1873
|
-
#
|
|
1874
|
-
#
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
GATE_BODY_TMPFILE="$(mktemp)"
|
|
2383
|
+
# Scope the scratch name to both this finding and this orchestrator process.
|
|
2384
|
+
# The marker read-back below detects a plausible-looking body substitution.
|
|
2385
|
+
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
2386
|
+
AGENT_TOKEN="${AGENT_ID:-${HOSTNAME:-orchestrator}-$$}"
|
|
2387
|
+
mkdir -p "$SCRATCHPAD"
|
|
2388
|
+
GATE_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_freshness-gate_${AGENT_TOKEN}"
|
|
2389
|
+
GATE_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_freshness-gate_${AGENT_TOKEN}.XXXXXX.md")"
|
|
1881
2390
|
printf '%s' "<!-- FORGE:GATE_FAILURE -->
|
|
1882
2391
|
## Code Branch Repair Skipped — Freshness Re-fetch Failed
|
|
1883
2392
|
|
|
1884
|
-
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the freshness re-fetch (\`gh issue view --json updatedAt\`) failed, so the concurrency guard could not confirm the issue body is still at the snapshot captured earlier in this iteration (\`${FINDING_UPDATED_AT_SNAPSHOT}\`). Proceeding with the write here would risk silently clobbering a concurrent edit with no way to verify. Human review required to confirm the current body state and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2565 -->
|
|
2393
|
+
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the freshness re-fetch (\`gh issue view --json updatedAt\`) failed, so the concurrency guard could not confirm the issue body is still at the snapshot captured earlier in this iteration (\`${FINDING_UPDATED_AT_SNAPSHOT}\`). Proceeding with the write here would risk silently clobbering a concurrent edit with no way to verify. Human review required to confirm the current body state and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2565 -->
|
|
2394
|
+
<!-- ${GATE_BODY_MARKER} -->" > "$GATE_BODY_TMPFILE"
|
|
1885
2395
|
gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$GATE_BODY_TMPFILE" 2>/dev/null || true
|
|
2396
|
+
gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${GATE_BODY_MARKER} -->" || { echo "ERROR: freshness-gate comment marker missing" >&2; exit 1; }
|
|
1886
2397
|
rm -f "$GATE_BODY_TMPFILE"
|
|
1887
2398
|
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # freshness-recheck-failure path
|
|
1888
2399
|
elif [ "$FINDING_UPDATED_AT_CURRENT" != "$FINDING_UPDATED_AT_SNAPSHOT" ]; then
|
|
1889
2400
|
echo "REPAIR: #${FINDING_NUM} skipped — concurrent edit detected (updatedAt changed from ${FINDING_UPDATED_AT_SNAPSHOT} to ${FINDING_UPDATED_AT_CURRENT} since this iteration's initial read); flagging instead of repairing"
|
|
1890
|
-
|
|
1891
|
-
CONCURRENT_EDIT_BODY_TMPFILE="$(mktemp)"
|
|
2401
|
+
CONCURRENT_EDIT_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_concurrent-edit_${AGENT_TOKEN}"
|
|
2402
|
+
CONCURRENT_EDIT_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_concurrent-edit_${AGENT_TOKEN}.XXXXXX.md")"
|
|
1892
2403
|
printf '%s' "<!-- FORGE:GATE_FAILURE -->
|
|
1893
2404
|
## Code Branch Repair Skipped — Concurrent Edit Detected
|
|
1894
2405
|
|
|
1895
|
-
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the issue body was edited concurrently (\`updatedAt\` changed from \`${FINDING_UPDATED_AT_SNAPSHOT}\` to \`${FINDING_UPDATED_AT_CURRENT}\` between this run's initial read and the repair write). Overwriting the body now would have silently discarded that concurrent edit. Human review required to reconcile and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2512 -->
|
|
2406
|
+
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair was skipped because the issue body was edited concurrently (\`updatedAt\` changed from \`${FINDING_UPDATED_AT_SNAPSHOT}\` to \`${FINDING_UPDATED_AT_CURRENT}\` between this run's initial read and the repair write). Overwriting the body now would have silently discarded that concurrent edit. Human review required to reconcile and, if still needed, re-apply the Code branch stamp manually. <!-- forge#2512 -->
|
|
2407
|
+
<!-- ${CONCURRENT_EDIT_BODY_MARKER} -->" > "$CONCURRENT_EDIT_BODY_TMPFILE"
|
|
1896
2408
|
gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$CONCURRENT_EDIT_BODY_TMPFILE" 2>/dev/null || true
|
|
2409
|
+
gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${CONCURRENT_EDIT_BODY_MARKER} -->" || { echo "ERROR: concurrent-edit comment marker missing" >&2; exit 1; }
|
|
1897
2410
|
rm -f "$CONCURRENT_EDIT_BODY_TMPFILE"
|
|
1898
2411
|
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # concurrent-edit path
|
|
1899
2412
|
else
|
|
@@ -1904,13 +2417,15 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
1904
2417
|
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,updatedAt \
|
|
1905
2418
|
--jq '{labels: [.labels[].name], title: .title, body: .body, updatedAt: .updatedAt}')
|
|
1906
2419
|
else
|
|
1907
|
-
|
|
1908
|
-
REPAIR_FAILED_BODY_TMPFILE="$(mktemp)"
|
|
2420
|
+
REPAIR_FAILED_BODY_MARKER="FORGE:BODY-INTEGRITY:${FINDING_NUM}_repair-failure_${AGENT_TOKEN}"
|
|
2421
|
+
REPAIR_FAILED_BODY_TMPFILE="$(mktemp "$SCRATCHPAD/${FINDING_NUM}_repair-failure_${AGENT_TOKEN}.XXXXXX.md")"
|
|
1909
2422
|
printf '%s' "<!-- FORGE:GATE_FAILURE -->
|
|
1910
2423
|
## Code Branch Repair Failed
|
|
1911
2424
|
|
|
1912
|
-
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair (\`gh issue edit --body\`) failed. Without this annotation, \`/work-on\`'s investigation phase may look for the code on the wrong branch (staging, where it is absent) and misclassify this confirmed finding as invalid. Human review required. <!-- forge#2443 -->
|
|
2425
|
+
Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${REPAIR_SOURCE_PR} bases on \`${REPAIR_PARENT_BASE}\` — not the staging fast lane. Automatic repair (\`gh issue edit --body\`) failed. Without this annotation, \`/work-on\`'s investigation phase may look for the code on the wrong branch (staging, where it is absent) and misclassify this confirmed finding as invalid. Human review required. <!-- forge#2443 -->
|
|
2426
|
+
<!-- ${REPAIR_FAILED_BODY_MARKER} -->" > "$REPAIR_FAILED_BODY_TMPFILE"
|
|
1913
2427
|
gh issue comment "$FINDING_NUM" -R {GH_REPO} --body-file "$REPAIR_FAILED_BODY_TMPFILE" 2>/dev/null || true
|
|
2428
|
+
gh api "repos/{GH_REPO}/issues/${FINDING_NUM}/comments" --jq '.[].body' | grep -Fqx "<!-- ${REPAIR_FAILED_BODY_MARKER} -->" || { echo "ERROR: repair-failure comment marker missing" >&2; exit 1; }
|
|
1914
2429
|
rm -f "$REPAIR_FAILED_BODY_TMPFILE"
|
|
1915
2430
|
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label needs-human 2>/dev/null || true # repair-failure path
|
|
1916
2431
|
fi
|
|
@@ -1947,7 +2462,8 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
1947
2462
|
# all" sets this to false so an operator draining a backlog is never idle-gated.
|
|
1948
2463
|
if [ "$CASCADE_DEFER_ON_BATCH_GATED" = "true" ] && [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
|
|
1949
2464
|
DEFER=true; DEFER_REASON="batch fully human-gated — idle policy"
|
|
1950
|
-
# Heuristic 1: Generation check — source issue has review-finding label
|
|
2465
|
+
# Heuristic 1: Generation check — source issue has review-finding label. Always defer
|
|
2466
|
+
# individual dispatch; only bounded P3 aggregation can consume an eligible member.
|
|
1951
2467
|
# NOT gated by orchestration.cascade.max_generation — this is Step 4C's autonomous
|
|
1952
2468
|
# mid-run cascade cap, which stays absolute regardless of config (see forge#2231's
|
|
1953
2469
|
# scope note above and phase-1-resolve.md's Cascade / Review-Finding Resolution
|
|
@@ -1956,9 +2472,17 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
1956
2472
|
elif SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
|
|
1957
2473
|
[ -n "$SOURCE_NUM" ] && \
|
|
1958
2474
|
gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2475
|
+
FINDING_GENERATION=$(compute_finding_generation "$(echo "$FINDING_DATA" | jq -r '.body')")
|
|
2476
|
+
FINDING_GENERATIONS[$FINDING_NUM]="$FINDING_GENERATION"
|
|
2477
|
+
DEFER=true; DEFER_REASON="generation ${FINDING_GENERATION} (source #${SOURCE_NUM} is also a review-finding)"
|
|
2478
|
+
if [ "$PRIORITY" = "P3" ] && [ "$FINDING_GENERATION" -le "$BATCH_MAX_GENERATION" ]; then
|
|
2479
|
+
# Keep this out of individual cascade dispatch, but offer it to the P3 batching
|
|
2480
|
+
# pass below. A singleton remains deferred; only a formed batch is admitted.
|
|
2481
|
+
BATCHABLE_DEFERRED_P3+=("$FINDING_NUM")
|
|
2482
|
+
DEFER_REASON="generation ${FINDING_GENERATION} — eligible for bounded P3 batching only"
|
|
2483
|
+
fi
|
|
2484
|
+
# Priority override: P0/P1 always execute; P2 is eligible for batch planning.
|
|
2485
|
+
elif [ "$PRIORITY" = "P0" ] || [ "$PRIORITY" = "P1" ]; then
|
|
1962
2486
|
DEFER=false
|
|
1963
2487
|
# Heuristic 2: Comment/typo keyword (only applies to P3 and below)
|
|
1964
2488
|
# Gated by CASCADE_KEYWORD_HEURISTIC (orchestration.cascade, forge#2234).
|
|
@@ -1978,6 +2502,20 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
1978
2502
|
DEFER=false
|
|
1979
2503
|
fi
|
|
1980
2504
|
|
|
2505
|
+
[ -n "${FINDING_GENERATIONS[$FINDING_NUM]:-}" ] || FINDING_GENERATIONS[$FINDING_NUM]=1
|
|
2506
|
+
|
|
2507
|
+
# Opt-in only: never defer a new-surface or P1/P2 finding because another
|
|
2508
|
+
# lineage is noisy. Step 4F re-evaluates this explicit deferral after drain.
|
|
2509
|
+
AMPLIFICATION_RATIO=$(awk "BEGIN { if ($MERGED_UNITS) printf \"%.2f\", $FINDINGS_SPAWNED / $MERGED_UNITS; else print 0 }")
|
|
2510
|
+
if [ "$DEFER" = "false" ] && [ "$FINDING_IS_REFINEMENT" = "true" ] && \
|
|
2511
|
+
[ "$PRIORITY" != "P1" ] && [ "$PRIORITY" != "P2" ] && \
|
|
2512
|
+
[ "$CASCADE_MAX_AMPLIFICATION" != "off" ] && \
|
|
2513
|
+
awk "BEGIN { exit !($AMPLIFICATION_RATIO > $CASCADE_MAX_AMPLIFICATION) }"; then
|
|
2514
|
+
DEFER=true
|
|
2515
|
+
DEFER_REASON="amplification bound exceeded (same-lineage refinement; ${AMPLIFICATION_RATIO} > ${CASCADE_MAX_AMPLIFICATION})"
|
|
2516
|
+
AMPLIFICATION_DEFERRED+=("$FINDING_NUM")
|
|
2517
|
+
fi
|
|
2518
|
+
|
|
1981
2519
|
if [ "$DEFER" = "true" ]; then
|
|
1982
2520
|
DEFERRED_FINDINGS+=($FINDING_NUM)
|
|
1983
2521
|
DEFERRED_REASONS[$FINDING_NUM]="$DEFER_REASON"
|
|
@@ -1988,9 +2526,11 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
1988
2526
|
done
|
|
1989
2527
|
```
|
|
1990
2528
|
|
|
1991
|
-
**
|
|
2529
|
+
**Concern-level batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
|
|
2530
|
+
|
|
2531
|
+
Cascade-spawned findings collected within a single `/orchestrate` run must be reconsidered against the complete open, unbatched, undispatched candidate registry, not only `QUEUED_FINDINGS` from this completion cycle. At initial resolution and after every five completions (or whenever `DEFERRED_FINDINGS` reaches `MAX_CONCURRENT`), collect retained ungrouped candidates, new findings, and open batch membership; call `planP3BatchGroups()` with that registry and `batchExclusionReason()` danger-zone inputs. Execute returned `extensions` before new groups, retain `ungrouped` members for the next sweep, and record group kinds or exclusion predicates in the run summary. <!-- Added: forge#2858; extended: forge#2851, forge#2852 -->
|
|
1992
2532
|
|
|
1993
|
-
|
|
2533
|
+
The per-cycle `QUEUED_FINDINGS` loop below remains the action-creation mechanism, but it MUST consume the complete-sweep planner output rather than decide groups from its local same-file map. Do not dispatch a locally ungrouped member until the next periodic sweep has considered it against the retained candidate set.
|
|
1994
2534
|
|
|
1995
2535
|
```bash
|
|
1996
2536
|
# Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
|
|
@@ -2006,13 +2546,17 @@ Cascade-spawned findings collected within a single `/orchestrate` run never pass
|
|
|
2006
2546
|
unset SURFACE_FILE_MEMBERS
|
|
2007
2547
|
declare -A SURFACE_FILE_MEMBERS
|
|
2008
2548
|
|
|
2009
|
-
#
|
|
2549
|
+
# Include generation-2 P3 findings only as batch candidates. They remain deferred
|
|
2550
|
+
# unless this pass actually forms a bounded batch; no member is individually queued.
|
|
2551
|
+
BATCHING_CANDIDATES=("${QUEUED_FINDINGS[@]}" "${BATCHABLE_DEFERRED_P3[@]}")
|
|
2552
|
+
|
|
2553
|
+
# Defensive cap on gh issue view fan-out. BATCHING_CANDIDATES is already bounded by
|
|
2010
2554
|
# upstream cascade control; this cap holds even if that bound is later loosened,
|
|
2011
2555
|
# so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
|
|
2012
2556
|
MAX_BATCH_SCAN=50
|
|
2013
2557
|
SCANNED=0
|
|
2014
2558
|
|
|
2015
|
-
for FINDING_NUM in "${
|
|
2559
|
+
for FINDING_NUM in "${BATCHING_CANDIDATES[@]}"; do
|
|
2016
2560
|
SCANNED=$((SCANNED + 1))
|
|
2017
2561
|
if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
|
|
2018
2562
|
echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
|
|
@@ -2022,7 +2566,8 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
|
2022
2566
|
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
|
|
2023
2567
|
--jq '{title: .title, body: .body, labels: [.labels[].name]}')
|
|
2024
2568
|
|
|
2025
|
-
#
|
|
2569
|
+
# Billing is never batched. Security findings are classified below and may
|
|
2570
|
+
# batch only with their exact same class. Same jq test() engine and
|
|
2026
2571
|
# patterns as phase-1-resolve.md's batching rule (single shared mechanism).
|
|
2027
2572
|
# Word-boundary anchored (not bare substrings) and attribution-boilerplate
|
|
2028
2573
|
# (**Confidence**/**Severity**/**Review comment** — see forge#2477 note below
|
|
@@ -2050,9 +2595,9 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
|
2050
2595
|
# keyword is not auto-batched) for closing a real bypass — the safe
|
|
2051
2596
|
# direction for a security-relevant exclusion. <!-- forge#2477 -->
|
|
2052
2597
|
echo "$FINDING_DATA" | jq -e '
|
|
2053
|
-
(.title | test("\\b(
|
|
2054
|
-
or ((.body | gsub("(?m)^\\*\\*(?:Confidence\\*\\*: (?:CONFIRMED|LIKELY|POSSIBLE)|Severity\\*\\*: (?:CRITICAL|HIGH|MEDIUM|LOW|INFO)|Review comment\\*\\*: https?://\\S+)$"; "")) | test("## Problem[\\s\\S]{0,500}\\b(
|
|
2055
|
-
or ([.labels[]] | any(. == "
|
|
2598
|
+
(.title | test("\\b(billing|operator-only|manual action required|human action required)\\b"; "i"))
|
|
2599
|
+
or ((.body | gsub("(?m)^\\*\\*(?:Confidence\\*\\*: (?:CONFIRMED|LIKELY|POSSIBLE)|Severity\\*\\*: (?:CRITICAL|HIGH|MEDIUM|LOW|INFO)|Review comment\\*\\*: https?://\\S+)$"; "")) | test("## Problem[\\s\\S]{0,500}\\b(billing|operator-only|manual action required|human action required)\\b"; "i"))
|
|
2600
|
+
or ([.labels[]] | any(. == "billing" or . == "needs-human" or . == "blocked" or . == "operator-only"))
|
|
2056
2601
|
' >/dev/null && continue
|
|
2057
2602
|
|
|
2058
2603
|
# Only P3 findings are eligible (P1/P2 already dispatched individually above).
|
|
@@ -2062,7 +2607,11 @@ for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
|
2062
2607
|
FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
|
|
2063
2608
|
[ -z "$FINDING_FILE" ] && continue
|
|
2064
2609
|
|
|
2065
|
-
|
|
2610
|
+
SAFETY_CLASS=$(node -e 'import("./bin/engine/admission.mjs").then(({ classifyBatchSafety }) => process.stdout.write(classifyBatchSafety(process.argv[1]) || "routine"))' "$(echo "$FINDING_DATA" | jq -r '.title + "\n" + .body + "\n" + (.labels | join(" "))')")
|
|
2611
|
+
# Same file is not enough for security work: the class key prevents a
|
|
2612
|
+
# credential finding from sharing a batch with injection/auth hardening.
|
|
2613
|
+
SURFACE_KEY="${SAFETY_CLASS}:${FINDING_FILE}"
|
|
2614
|
+
SURFACE_FILE_MEMBERS["$SURFACE_KEY"]="${SURFACE_FILE_MEMBERS[$SURFACE_KEY]} $FINDING_NUM"
|
|
2066
2615
|
done
|
|
2067
2616
|
|
|
2068
2617
|
# For each same-file cluster of 2+, actually CREATE the batch issue (executable —
|
|
@@ -2073,23 +2622,39 @@ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
|
|
|
2073
2622
|
MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
|
|
2074
2623
|
[ "${#MEMBERS[@]}" -ge 2 ] || continue
|
|
2075
2624
|
|
|
2625
|
+
SAFETY_CLASS=${FILE%%:*}
|
|
2626
|
+
SURFACE_FILE=${FILE#*:}
|
|
2627
|
+
|
|
2076
2628
|
# Sanitize the affected-file path before interpolating it into the issue title/body.
|
|
2077
2629
|
# Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
|
|
2078
2630
|
# restrict to a validated charset so the value cannot break the gh argument
|
|
2079
2631
|
# boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
|
|
2080
|
-
SAFE_SURFACE_AREA=$(printf '%s' "$
|
|
2632
|
+
SAFE_SURFACE_AREA=$(printf '%s' "$SURFACE_FILE" | tr -cd 'A-Za-z0-9._/-')
|
|
2081
2633
|
|
|
2082
|
-
echo "Same-run surface-area cluster: ${#MEMBERS[@]}
|
|
2634
|
+
echo "Same-run surface-area cluster: ${#MEMBERS[@]} ${SAFETY_CLASS} findings share $SURFACE_FILE — creating batch issue(s)"
|
|
2083
2635
|
|
|
2084
|
-
#
|
|
2085
|
-
|
|
2086
|
-
|
|
2636
|
+
# Routine batches cap at 8; security-class batches cap at 3 and their body
|
|
2637
|
+
# must state a live-vector or defence-in-depth verdict for every member.
|
|
2638
|
+
BATCH_CAP=8
|
|
2639
|
+
[ "$SAFETY_CLASS" != "routine" ] && BATCH_CAP=3
|
|
2640
|
+
for START in $(seq 0 "$BATCH_CAP" $(( ${#MEMBERS[@]} - 1 ))); do
|
|
2641
|
+
CHUNK=("${MEMBERS[@]:$START:$BATCH_CAP}")
|
|
2087
2642
|
[ "${#CHUNK[@]}" -ge 2 ] || continue
|
|
2088
2643
|
|
|
2089
2644
|
MEMBER_LINES=""
|
|
2645
|
+
BATCH_MEMBER_GENERATION=1
|
|
2646
|
+
GEN2_MEMBER_LINES=""
|
|
2090
2647
|
for M in "${CHUNK[@]}"; do
|
|
2091
2648
|
MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
|
|
2092
2649
|
MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
|
|
2650
|
+
MEMBER_GENERATION="${FINDING_GENERATIONS[$M]:-1}"
|
|
2651
|
+
if [ "$MEMBER_GENERATION" -gt "$BATCH_MEMBER_GENERATION" ]; then
|
|
2652
|
+
BATCH_MEMBER_GENERATION="$MEMBER_GENERATION"
|
|
2653
|
+
fi
|
|
2654
|
+
if [ "$MEMBER_GENERATION" -ge 2 ]; then
|
|
2655
|
+
GEN2_MEMBER_LINES="${GEN2_MEMBER_LINES}- #${M}: generation ${MEMBER_GENERATION}"$'\n'
|
|
2656
|
+
fi
|
|
2657
|
+
[ "$SAFETY_CLASS" = "routine" ] || MEMBER_LINES="${MEMBER_LINES} - **Verdict**: [ ] live vector [ ] defence-in-depth"$'\n'
|
|
2093
2658
|
done
|
|
2094
2659
|
|
|
2095
2660
|
# Dedup-check with member exclusion (MANDATORY — forge#2432, identical mechanism to
|
|
@@ -2115,10 +2680,8 @@ for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
|
|
|
2115
2680
|
continue
|
|
2116
2681
|
fi
|
|
2117
2682
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
--label "review-finding,priority:P3,batch" \
|
|
2121
|
-
--body "$(cat <<BATCH_EOF
|
|
2683
|
+
CREATE_TOKEN="forge-create-$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM"
|
|
2684
|
+
CREATE_BODY="$(cat <<BATCH_EOF
|
|
2122
2685
|
## Problem
|
|
2123
2686
|
|
|
2124
2687
|
Batch of P3 review findings in **${SAFE_SURFACE_AREA}** (same file), clustered mid-run by phase-4-execution.md to reduce per-finding pipeline overhead.
|
|
@@ -2128,6 +2691,12 @@ Batch of P3 review findings in **${SAFE_SURFACE_AREA}** (same file), clustered m
|
|
|
2128
2691
|
<!-- FORGE:BATCH_MEMBERS -->
|
|
2129
2692
|
${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
|
|
2130
2693
|
|
|
2694
|
+
**Maximum member generation**: ${BATCH_MEMBER_GENERATION}
|
|
2695
|
+
<!-- FORGE:BATCH_MAX_GENERATION: ${BATCH_MEMBER_GENERATION} --> <!-- allowlist:check-spec-markers -->
|
|
2696
|
+
|
|
2697
|
+
**Generation >= 2 members admitted by bounded batching**:
|
|
2698
|
+
${GEN2_MEMBER_LINES:-none}
|
|
2699
|
+
|
|
2131
2700
|
## Acceptance Criteria
|
|
2132
2701
|
|
|
2133
2702
|
- [ ] All member findings addressed or closed as false-positive
|
|
@@ -2136,7 +2705,26 @@ ${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
|
|
|
2136
2705
|
|
|
2137
2706
|
<!-- FORGE:BATCHABLE -->
|
|
2138
2707
|
BATCH_EOF
|
|
2139
|
-
)"
|
|
2708
|
+
)"
|
|
2709
|
+
CREATE_BODY="${CREATE_BODY}
|
|
2710
|
+
|
|
2711
|
+
<!-- issue-create-token:${CREATE_TOKEN} -->"
|
|
2712
|
+
CREATE_RESPONSE=$(gh api "repos/{GH_REPO}/issues" --method POST \
|
|
2713
|
+
-f title="$PROPOSED_BATCH_TITLE" -f body="$CREATE_BODY" \
|
|
2714
|
+
-f 'labels[]=review-finding' -f 'labels[]=priority:P3' -f 'labels[]=batch') || {
|
|
2715
|
+
echo "ERROR: GitHub rejected same-run batch creation; members remain queued." >&2
|
|
2716
|
+
continue
|
|
2717
|
+
}
|
|
2718
|
+
BATCH_ISSUE_NUM=$(echo "$CREATE_RESPONSE" | jq -r '.number // empty')
|
|
2719
|
+
if [ -z "$BATCH_ISSUE_NUM" ]; then
|
|
2720
|
+
echo "ERROR: same-run batch creation returned no issue number; members remain queued." >&2
|
|
2721
|
+
continue
|
|
2722
|
+
fi
|
|
2723
|
+
CREATED_BODY=$(gh issue view "$BATCH_ISSUE_NUM" -R {GH_REPO} --json body --jq '.body') || continue
|
|
2724
|
+
if ! printf '%s' "$CREATED_BODY" | grep -qF "issue-create-token:${CREATE_TOKEN}"; then
|
|
2725
|
+
echo "ERROR: batch issue #${BATCH_ISSUE_NUM} failed create-token read-back; members remain queued." >&2
|
|
2726
|
+
continue
|
|
2727
|
+
fi
|
|
2140
2728
|
|
|
2141
2729
|
# Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
|
|
2142
2730
|
# with the single batch issue, so the dispatch step below operates on the
|
|
@@ -2635,6 +3223,8 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
2635
3223
|
**Repository**: {GH_REPO}
|
|
2636
3224
|
**Repo path**: {REPO_PATH}
|
|
2637
3225
|
|
|
3226
|
+
**KNOWLEDGE GIST CAPABILITY**: This orchestration already probed it: `${FORGE_GIST_CAPABLE}`. Before invoking `/work-on`, run `export FORGE_GIST_CAPABLE=${FORGE_GIST_CAPABLE}`. Do not re-probe or attempt Gist creation when it is `false`.
|
|
3227
|
+
|
|
2638
3228
|
**YOUR MISSION**: Invoke \`/work-on\` via the Skill tool and let it run to completion. \`/work-on\` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — \`/work-on\` handles everything including issue closure and label updates in its close phase.
|
|
2639
3229
|
|
|
2640
3230
|
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, \`needs-human\`, or \`workflow:awaiting-merge\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
|