forgedock 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
install:
|
|
2
|
+
install: core
|
|
3
3
|
---
|
|
4
4
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
5
5
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
@@ -8,6 +8,136 @@ install: internal
|
|
|
8
8
|
|
|
9
9
|
## Phase 4: Streaming DAG Execution
|
|
10
10
|
|
|
11
|
+
**Entry requires `phase-3-dependency.md`'s Step 3D.6 completion gate to have passed.** <!-- Added: forge#1913 --> Every step below (budget init, dispatch, dependency resolution) reads `ISSUES[]`, `PREDECESSORS[]`, `ISSUE_DOMAIN[]`, `ISSUE_SCORE[]`, `ISSUE_COST_ESTIMATE[]`, and `ISSUE_HAS_PRIOR[]` as authoritative Phase 3 output — none of these are re-derived here. If Phase 4 is being entered without having actually run Phase 3's Steps 3A–3E.5 in this session (e.g. a fresh session resuming mid-batch), reconstruct from GitHub via the "Orchestrator state reconstruction on wake / after compaction" procedure in `phase-3-dependency.md` rather than assuming these variables are already populated.
|
|
12
|
+
|
|
13
|
+
### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
|
|
14
|
+
|
|
15
|
+
Initialize budget tracking state before the first dispatch. Read `--budget N` from the orchestrator's argument list (passed from the top-level `/orchestrate` invocation). When `--budget` is not set, `BUDGET_LIMIT` is `Infinity` (uncapped — current default behavior preserved).
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# --- Budget initialization ---
|
|
19
|
+
# Parse --budget N from ARGUMENTS (e.g. /orchestrate fast-lane --budget 5.00)
|
|
20
|
+
BUDGET_LIMIT=$(echo "${ARGUMENTS:-}" | grep -oP '(?<=--budget )\S+' | head -1 || echo "")
|
|
21
|
+
if [ -z "$BUDGET_LIMIT" ] || ! echo "$BUDGET_LIMIT" | grep -qP '^\d+(\.\d+)?$'; then
|
|
22
|
+
BUDGET_LIMIT="Infinity"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
PROJECTED_SPEND="0" # sum of ISSUE_COST_ESTIMATE[] for dispatched issues
|
|
26
|
+
ACTUAL_SPEND="0" # sum of actual cost reported by completed agents (best-effort)
|
|
27
|
+
DEFERRED_BUDGET_ISSUES=() # issues deferred because projected spend would exceed budget
|
|
28
|
+
EPSILON_DISPATCHED=false # true once at least one ε-reserve issue has been dispatched
|
|
29
|
+
|
|
30
|
+
if [ "$BUDGET_LIMIT" != "Infinity" ]; then
|
|
31
|
+
EPSILON_BUDGET=$(echo "scale=4; $BUDGET_LIMIT * 0.10" | bc 2>/dev/null || echo "0")
|
|
32
|
+
echo "Budget initialized: BUDGET_LIMIT=\$${BUDGET_LIMIT} EPSILON_BUDGET=\$${EPSILON_BUDGET} (10% ε-reserve)"
|
|
33
|
+
echo "Issues without cost priors (ε-reserve eligible): ${NO_PRIOR_ISSUES[*]:-none}"
|
|
34
|
+
else
|
|
35
|
+
EPSILON_BUDGET="0"
|
|
36
|
+
echo "Budget: uncapped (no --budget flag) — dispatching all ready issues by score order"
|
|
37
|
+
fi
|
|
38
|
+
# --- End budget initialization ---
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Budget halt condition** (checked in Step 4A before each dispatch):
|
|
42
|
+
|
|
43
|
+
Before dispatching issue `NUM`, check whether its estimated cost would exceed the remaining budget:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Check budget before dispatching NUM
|
|
47
|
+
should_dispatch() {
|
|
48
|
+
local NUM="$1"
|
|
49
|
+
local COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
|
|
50
|
+
|
|
51
|
+
if [ "$BUDGET_LIMIT" = "Infinity" ]; then
|
|
52
|
+
return 0 # uncapped — always dispatch
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
NEW_PROJECTED=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
|
|
56
|
+
MAIN_CEILING=$(echo "scale=4; $BUDGET_LIMIT - $EPSILON_BUDGET" | bc 2>/dev/null || echo "$BUDGET_LIMIT")
|
|
57
|
+
|
|
58
|
+
# ε-reserve logic: if this is a no-prior issue AND ε-budget has not yet been
|
|
59
|
+
# used AND the no-prior issue has NOT been dispatched yet, allow it even if
|
|
60
|
+
# the main ceiling is hit (up to BUDGET_LIMIT total).
|
|
61
|
+
if [ "${ISSUE_HAS_PRIOR[$NUM]:-false}" = "false" ] && [ "$EPSILON_DISPATCHED" = "false" ]; then
|
|
62
|
+
if echo "$NEW_PROJECTED $BUDGET_LIMIT" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
|
|
63
|
+
EPSILON_DISPATCHED=true
|
|
64
|
+
echo "ε-reserve: dispatching #${NUM} (no-prior issue) from exploration reserve"
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Main budget check: defer if projected would exceed main ceiling
|
|
70
|
+
if echo "$NEW_PROJECTED $MAIN_CEILING" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
|
|
71
|
+
DEFERRED_BUDGET_ISSUES+=("$NUM")
|
|
72
|
+
echo "BUDGET DEFER: #${NUM} (est. \$${COST}) would push projected spend to \$${NEW_PROJECTED} > main ceiling \$${MAIN_CEILING}"
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Budget deferred-issues report** (output when `BUDGET_LIMIT` is finite and `DEFERRED_BUDGET_ISSUES` is non-empty — print at end of Phase 4, before Phase 5):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
## Budget Report
|
|
84
|
+
|
|
85
|
+
**Budget limit**: $${BUDGET_LIMIT}
|
|
86
|
+
**Projected spend (dispatched issues)**: $${PROJECTED_SPEND}
|
|
87
|
+
**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = $${EPSILON_BUDGET})
|
|
88
|
+
|
|
89
|
+
### Deferred Issues (budget exhausted — never silently dropped)
|
|
90
|
+
|
|
91
|
+
| Issue | Title | Score | Est. Cost | Reason |
|
|
92
|
+
|-------|-------|-------|-----------|--------|
|
|
93
|
+
| #{N} | {title} | {score} | ${cost} | Budget ceiling reached |
|
|
94
|
+
|
|
95
|
+
**Action**: Re-run `/orchestrate {deferred_issue_numbers} [--budget N]` to process deferred issues, or increase `--budget`.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
|
|
99
|
+
|
|
100
|
+
### Step 4A-pre.0.2: Concurrency gate initialization (MANDATORY) <!-- Added: forge#1912 -->
|
|
101
|
+
|
|
102
|
+
**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.
|
|
103
|
+
|
|
104
|
+
Initialize the concurrency cap and its batch-scope tracking state before the first dispatch:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# --- Concurrency gate initialization ---
|
|
108
|
+
MAX_CONCURRENT=$(yq '.orchestration.max_concurrent // 12' forge.yaml 2>/dev/null || echo 12)
|
|
109
|
+
|
|
110
|
+
# Validate MAX_CONCURRENT is a bare positive integer before it is ever used in arithmetic
|
|
111
|
+
# expansion or array-slice-length position below. Bash's `$(( ))` and `${arr[@]:off:len}`
|
|
112
|
+
# both re-evaluate an unquoted variable's contents as an expression — an unvalidated value
|
|
113
|
+
# (e.g. a malicious or malformed forge.yaml, or a stray non-numeric string) can inject
|
|
114
|
+
# arbitrary command substitution into the arithmetic context, and 0/negative values make
|
|
115
|
+
# the chunked dispatch loop below (Step 4A) never advance its index, spinning forever.
|
|
116
|
+
# Same validation convention as BUDGET_LIMIT in Step 4A-pre.0 above. <!-- Added: forge#1912 -->
|
|
117
|
+
if [ "$MAX_CONCURRENT" = "null" ] || ! echo "$MAX_CONCURRENT" | grep -qP '^[1-9][0-9]*$'; then
|
|
118
|
+
echo "WARNING: forge.yaml → orchestration.max_concurrent is not a positive integer (\"${MAX_CONCURRENT}\") — falling back to default 12"
|
|
119
|
+
MAX_CONCURRENT=12
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
ACTIVE_DISPATCH_COUNT=0 # in-flight dispatched-but-not-yet-completed agents, this batch
|
|
123
|
+
DEFERRED_CONCURRENCY_ISSUES=() # ready issues held back because no headroom was available
|
|
124
|
+
|
|
125
|
+
echo "Concurrency gate initialized: MAX_CONCURRENT=${MAX_CONCURRENT} (forge.yaml → orchestration.max_concurrent; default 12 when unset or invalid)"
|
|
126
|
+
# --- End concurrency gate initialization ---
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Headroom helper** (used by every dispatch site below — Step 4A's engine-first loop, Step 4A's Agent-spawn-fallback path, and Step 4B's newly-ready dispatch):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
dispatch_headroom() {
|
|
133
|
+
local HEADROOM=$((MAX_CONCURRENT - ACTIVE_DISPATCH_COUNT))
|
|
134
|
+
[ "$HEADROOM" -lt 0 ] && HEADROOM=0
|
|
135
|
+
echo "$HEADROOM"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**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.
|
|
140
|
+
|
|
11
141
|
### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
|
|
12
142
|
|
|
13
143
|
**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.
|
|
@@ -135,7 +265,7 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
|
|
|
135
265
|
|
|
136
266
|
```bash
|
|
137
267
|
# Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
|
|
138
|
-
# (installed by `npx forgedock` — see bin/
|
|
268
|
+
# (installed by `npx forgedock` — see bin/journey.mjs: forge()'s linkPipelineScripts() step)
|
|
139
269
|
# Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
|
|
140
270
|
|
|
141
271
|
declare -A ISSUE_LANE
|
|
@@ -149,6 +279,43 @@ QUEUED_FINDINGS=()
|
|
|
149
279
|
declare -A DEFERRED_REASONS
|
|
150
280
|
declare -A AGENT_ISSUE_MAP
|
|
151
281
|
|
|
282
|
+
# Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
|
|
283
|
+
# dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
|
|
284
|
+
# EDGE_KIND/EDGE_FILES from phase-3-dependency.md Step 3C) resolves; consumed by Step 4A's
|
|
285
|
+
# {GIST_CONTEXT} generation. EDGE_BRIEFED guards against re-appending the same predecessor's
|
|
286
|
+
# brief on every completion cycle this loop re-runs before BLOCKED_NUM actually dispatches.
|
|
287
|
+
declare -A SAME_FILE_BRIEF
|
|
288
|
+
declare -A EDGE_BRIEFED
|
|
289
|
+
|
|
290
|
+
# Human-gated idle/backpressure flag (Step 4B item 6.7, forge#1814). Starts false —
|
|
291
|
+
# recomputed every completion cycle over {all_batch_issue_numbers}. Declared at batch
|
|
292
|
+
# scope (not per-agent) so Step 4C can read the latest value on every iteration.
|
|
293
|
+
BATCH_FULLY_GATED=false
|
|
294
|
+
|
|
295
|
+
# Per-batch token budget for Step 4C's review-finding cascade control (forge#1858).
|
|
296
|
+
# Distinct from the $-denominated `--budget N` flag (economic scheduling, forge#1743,
|
|
297
|
+
# Step 4A-pre.0 above) — that mechanism is opt-in and gates the *original* issue
|
|
298
|
+
# dispatch order; this ceiling is always-on, token-denominated, and scopes ONLY to
|
|
299
|
+
# Step 4C's cascade dispatch of review-finding issues. Declared once here (batch scope,
|
|
300
|
+
# not per-agent) so BATCH_TOKEN_SPEND accumulates correctly across every Step 4C run
|
|
301
|
+
# this session performs.
|
|
302
|
+
TOKEN_BUDGET=$(yq '.pipeline.token_budget_per_batch // 900000' forge.yaml 2>/dev/null || echo 900000)
|
|
303
|
+
TOKEN_ESTIMATE_PER_FINDING=$(yq '.pipeline.token_estimate_per_finding // 150000' forge.yaml 2>/dev/null || echo 150000)
|
|
304
|
+
BATCH_TOKEN_SPEND=0
|
|
305
|
+
TOKEN_DEFERRED=() # findings deferred by the token-budget rule this run (re-evaluable in Step 4F.2.6)
|
|
306
|
+
|
|
307
|
+
# Surface-area batching accumulators (forge#1818), promoted to batch scope here so the
|
|
308
|
+
# Step 6B report (forge#1858) sees the full-run total instead of only the last Step 4C
|
|
309
|
+
# completion cycle to touch them. SURFACE_FILE_MEMBERS (the per-cycle file->members grouping
|
|
310
|
+
# map used inside Step 4C) is rebuilt from the current QUEUED_FINDINGS every cycle and must
|
|
311
|
+
# carry no cross-cycle state of its own — but `declare -A` alone does NOT guarantee this: on
|
|
312
|
+
# an already-declared associative array it is a no-op on existing contents. Step 4C enforces
|
|
313
|
+
# the freshness guarantee explicitly via `unset SURFACE_FILE_MEMBERS` immediately before its
|
|
314
|
+
# `declare -A SURFACE_FILE_MEMBERS` each cycle (see below) — do not rely on re-declaration
|
|
315
|
+
# alone. <!-- Added: forge#1909 -->
|
|
316
|
+
SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch across the run
|
|
317
|
+
SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
|
|
318
|
+
|
|
152
319
|
for NUM in {ready_issue_numbers}; do
|
|
153
320
|
PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
|
|
154
321
|
echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
|
|
@@ -174,17 +341,43 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
|
|
|
174
341
|
**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.
|
|
175
342
|
|
|
176
343
|
```bash
|
|
177
|
-
# Engine-first dispatch: check CLI availability, then dispatch ready issues in
|
|
344
|
+
# Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
|
|
345
|
+
# Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
|
|
178
346
|
FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
|
|
179
347
|
|
|
180
348
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
349
|
+
# Build the budget-gated dispatch queue first (forge#1743's should_dispatch() still applies
|
|
350
|
+
# per-issue, independent of the concurrency cap below).
|
|
351
|
+
DISPATCH_QUEUE=()
|
|
352
|
+
for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
|
|
353
|
+
if should_dispatch "$NUM"; then
|
|
354
|
+
DISPATCH_QUEUE+=("$NUM")
|
|
355
|
+
fi # else: added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
|
|
356
|
+
done
|
|
357
|
+
|
|
358
|
+
# Concurrency gate (forge#1912): dispatch in chunks of MAX_CONCURRENT, waiting for each
|
|
359
|
+
# chunk to finish before starting the next. This is the code-form equivalent of the
|
|
360
|
+
# Agent-spawn-fallback cap below — never more than MAX_CONCURRENT `forgedock run-issue`
|
|
361
|
+
# processes in flight at once, regardless of how many issues are ready.
|
|
362
|
+
IDX=0
|
|
363
|
+
while [ "$IDX" -lt "${#DISPATCH_QUEUE[@]}" ]; do
|
|
364
|
+
CHUNK=("${DISPATCH_QUEUE[@]:$IDX:$MAX_CONCURRENT}")
|
|
365
|
+
echo "Dispatching chunk of ${#CHUNK[@]} issue(s) — concurrency cap ${MAX_CONCURRENT}"
|
|
366
|
+
|
|
367
|
+
for NUM in "${CHUNK[@]}"; do
|
|
368
|
+
LANE="${ISSUE_LANE[$NUM]}"
|
|
369
|
+
PR_BASE="${ISSUE_PR_BASE[$NUM]}"
|
|
370
|
+
COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
|
|
371
|
+
|
|
372
|
+
# Advance PROJECTED_SPEND before forking so subsequent iterations see the updated total
|
|
373
|
+
PROJECTED_SPEND=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
|
|
374
|
+
|
|
375
|
+
echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE (score=${ISSUE_SCORE[$NUM]:-?} est_cost=\$${COST} projected_total=\$${PROJECTED_SPEND})"
|
|
376
|
+
forgedock run-issue "$NUM" --lane "$PR_BASE" &
|
|
377
|
+
done
|
|
378
|
+
wait # cap enforced here — no more than MAX_CONCURRENT processes run concurrently
|
|
379
|
+
IDX=$((IDX + MAX_CONCURRENT))
|
|
186
380
|
done
|
|
187
|
-
wait
|
|
188
381
|
echo "Engine dispatch complete — advancing to Step 4B (completion sweep)"
|
|
189
382
|
else
|
|
190
383
|
echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
|
|
@@ -206,7 +399,7 @@ For each **ready** issue (all predecessors resolved or no predecessors), spawn a
|
|
|
206
399
|
```
|
|
207
400
|
Agent(
|
|
208
401
|
subagent_type="general-purpose",
|
|
209
|
-
model="
|
|
402
|
+
model="{SUBAGENT_MODEL}",
|
|
210
403
|
description="Work on {PROJECT_PREFIX}#{NUMBER}",
|
|
211
404
|
run_in_background=true,
|
|
212
405
|
prompt="You are working on GitHub issue #{NUMBER} for the {PROJECT_NAME} project.
|
|
@@ -217,21 +410,22 @@ Agent(
|
|
|
217
410
|
|
|
218
411
|
**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.
|
|
219
412
|
|
|
220
|
-
**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`, or `
|
|
413
|
+
**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.
|
|
221
414
|
|
|
222
415
|
**HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with `review-finding` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
|
|
223
416
|
|
|
224
417
|
**IMPORTANT RULES**:
|
|
225
418
|
- **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '{PROJECT_PREFIX}{NUMBER}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
|
|
226
|
-
- For default repo issues: `Skill(skill='work-on', args='{NUMBER}')`
|
|
227
|
-
- For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER}')` (prefix from forge.yaml → repos.satellites)
|
|
419
|
+
- For default repo issues: `Skill(skill='work-on', args='{NUMBER} --under-orchestration')`
|
|
420
|
+
- For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER} --under-orchestration')` (prefix from forge.yaml → repos.satellites)
|
|
421
|
+
- 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).
|
|
228
422
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
229
423
|
- NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
|
|
230
424
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
|
|
231
425
|
- If the issue is INVALID after investigation, close it with a comment explaining why
|
|
232
426
|
- If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
|
|
233
427
|
- Do not interact with the user — you are running autonomously in the background
|
|
234
|
-
- **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed
|
|
428
|
+
- **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed, then let `/review-pr`'s own verdict decide: APPROVED (no unresolved CONFIRMED HIGH/CRITICAL finding) → merge to `staging` and create follow-up issues for the rest, **regardless of domain**. Domain alone (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B) is NOT a reason to add `needs-human` and stop — `staging` is reversible; the real human deploy gate is `staging → main`, not this merge. <!-- Added: forge#1815 --> `needs-human` is reserved for what the pipeline genuinely cannot do itself: spend/procurement decisions, real-environment validation it has no access to, product/architecture judgment calls a human must make, or `/review-pr`'s existing evidence-based escalations (spec-evolution guard, novel task-type/module-combo trust escalation, calibration-based overconfidence routing). An unresolved CONFIRMED HIGH/CRITICAL finding is `/review-pr`'s own withheld-APPROVED case, not a domain-driven `needs-human` halt — `/review-pr` already refuses to return APPROVED when that's true, so there is no separate domain check to perform here.
|
|
235
429
|
|
|
236
430
|
**LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
|
|
237
431
|
After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the issue's current workflow label:
|
|
@@ -239,8 +433,9 @@ After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the is
|
|
|
239
433
|
gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))]'
|
|
240
434
|
```
|
|
241
435
|
**Terminal labels** (only these allow you to stop): `workflow:merged`, `workflow:invalid`
|
|
242
|
-
**Terminal condition also**: `needs-human` label present OR issue state is `closed`
|
|
243
|
-
|
|
436
|
+
**Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
|
|
437
|
+
`needs-human` and `workflow:awaiting-merge` are terminal-FOR-THIS-AGENT (this individual `/work-on` run stops here — a human decision or merge is now the blocking step) but are NOT "done" from the DAG's point of view; see Predecessor Classification in Step 4B for how the orchestrator's dependency logic treats them (`GATED`, not `DONE`).
|
|
438
|
+
If the label is NOT terminal (e.g., `workflow:investigating`, `workflow:ready-to-build`, `workflow:building`, `workflow:in-review`), invoke `Skill(skill='work-on', args='{NUMBER} --under-orchestration')` again immediately. The `/work-on` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
|
|
244
439
|
|
|
245
440
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
246
441
|
- If the issue has the `review-finding` label, read the issue body for `**Code branch**: \`{branch}\``
|
|
@@ -274,7 +469,11 @@ if [ -n "$SYNTHESIS_BRIEF" ]; then
|
|
|
274
469
|
${SYNTHESIS_BRIEF}"
|
|
275
470
|
else
|
|
276
471
|
# Fallback: Phase 2.5 did not run (0/1 investigations). Use the raw gist behavior.
|
|
472
|
+
# Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching,
|
|
473
|
+
# since sub-issue bodies commonly render the label as "**Parent**: #NNN" and the bare
|
|
474
|
+
# label alternation below would otherwise fail to match past the emphasis characters.
|
|
277
475
|
PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
|
|
476
|
+
| sed -E 's/[*_]+//g' \
|
|
278
477
|
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
279
478
|
|
|
280
479
|
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
@@ -297,6 +496,30 @@ fi
|
|
|
297
496
|
|
|
298
497
|
If `GIST_CONTEXT` is empty (no synthesis brief, no parent investigation, and no milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341, forge#1192 -->
|
|
299
498
|
|
|
499
|
+
**Claims board context injection** <!-- Added: forge#1736 -->: When a coordination issue exists for this batch (`FORGE_COORD_ISSUE` is set), append the claims board URL and the active-claims check instruction to the agent's context. This enables each `/work-on` agent to post its `FORGE:CLAIM` on build start.
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
# Inject coordination issue URL if claims board was created in Step 3D.1
|
|
503
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
|
|
504
|
+
GIST_CONTEXT="${GIST_CONTEXT}
|
|
505
|
+
|
|
506
|
+
**ORCHESTRATION CLAIMS BOARD**: This agent is running under an orchestration batch.
|
|
507
|
+
Claims board issue URL: ${FORGE_COORD_ISSUE}
|
|
508
|
+
|
|
509
|
+
On build start (Phase B2 / Phase 3C of /work-on), post a FORGE:CLAIM annotation on the
|
|
510
|
+
coordination issue above. Required fields:
|
|
511
|
+
Holder: ##{NUMBER} / batch-$(date -u +%Y%m%dT%H%M%S)
|
|
512
|
+
Files: (list of files from your FORGE:CONTRACT deliverables table, one per line)
|
|
513
|
+
Interfaces: (public function/type signatures you will modify or that callers must preserve)
|
|
514
|
+
TTL: terminal state of Holder issue ##{NUMBER}
|
|
515
|
+
|
|
516
|
+
On reaching terminal state (workflow:merged, workflow:invalid, needs-human, or workflow:awaiting-merge), post
|
|
517
|
+
<!-- FORGE:CLAIM_RELEASED --> on the coordination issue to release your claim.
|
|
518
|
+
|
|
519
|
+
Set FORGE_COORD_ISSUE=${FORGE_COORD_ISSUE} in your environment so /work-on phases can read it."
|
|
520
|
+
fi
|
|
521
|
+
```
|
|
522
|
+
|
|
300
523
|
**Hot-copy CONTRACT context** (extends `{GIST_CONTEXT}` for milestone-lane issues where the parent issue already carries a `FORGE:CONTRACT` annotation): <!-- Added: forge#1277 -->
|
|
301
524
|
|
|
302
525
|
When a DAG node issue was spawned from a decomposition (parent issue has `workflow:decomposed` label and the child issue body references `**Parent**: #NNN`), the parent issue may already have a `FORGE:CONTRACT` annotation that was posted before decomposition. Inject a scoped excerpt into the child's prompt so the child does not re-fetch it.
|
|
@@ -322,6 +545,20 @@ fi
|
|
|
322
545
|
|
|
323
546
|
If the issue has no parent reference, or the parent has no `FORGE:CONTRACT` annotation, this block produces no output and `GIST_CONTEXT` is unchanged. The hot-copy is an optimization — the durable annotation on the parent issue remains the authoritative record for compaction recovery.
|
|
324
547
|
|
|
548
|
+
**Same-file current-state brief injection** <!-- Added: forge#1860 --> — a separate, parallel mechanism to the `FORGE:SYNTHESIS_BRIEF` handling above: it does not replace or interact with Phase 2.5 investigation→implementation forwarding. When this issue was dispatched because a Layer 1/2/3 structural predecessor edge just resolved, Step 4B's core streaming dispatch loop has already populated `SAME_FILE_BRIEF[{NUMBER}]` with a short excerpt of what each such predecessor changed in the shared file/directory/module. Append it here:
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
# Append same-file/directory/shared-module briefs from resolved structural-edge predecessors.
|
|
552
|
+
if [ -n "${SAME_FILE_BRIEF[{NUMBER}]:-}" ]; then
|
|
553
|
+
GIST_CONTEXT="${GIST_CONTEXT}
|
|
554
|
+
|
|
555
|
+
**SAME-FILE STATE BRIEF (structural DAG predecessor)**: One or more predecessors in this batch were serialized against you because they touch the same file/directory/module (Step 3C Layer 1/2/3 — not an explicit \`Depends on\`). Here is what they just changed — use this as your starting understanding of the file's current state; do not re-investigate it cold:
|
|
556
|
+
${SAME_FILE_BRIEF[{NUMBER}]}"
|
|
557
|
+
fi
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
This block is a no-op — `GIST_CONTEXT` resolves exactly as before — whenever `SAME_FILE_BRIEF[{NUMBER}]` is unset: the issue had no predecessors, its only predecessors were explicit-dependency/DATABASE-chain/Layer 4/Layer 5 edges (none of which populate `SAME_FILE_BRIEF`), or the orchestrator session was compacted and restarted between Phase 3 and this dispatch (in which case `EDGE_KIND`/`EDGE_FILES`/`SAME_FILE_BRIEF` are in-memory-only and are not reconstructed by the wake/compaction recovery in `phase-3-dependency.md` — the dispatched agent simply proceeds without the brief, exactly as it would have before this mechanism existed).
|
|
561
|
+
|
|
325
562
|
**Capture agent IDs after the batch spawn (MANDATORY)**: Each `Agent(...)` call returns an agent ID. Store each returned ID in `AGENT_ISSUE_MAP` keyed by issue number. This map is the only way to resume a stalled agent by ID in Steps 4B and 4B.5:
|
|
326
563
|
|
|
327
564
|
```
|
|
@@ -331,63 +568,487 @@ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
|
|
|
331
568
|
|
|
332
569
|
`AGENT_ISSUE_MAP` starts empty and accumulates entries as agents are spawned. For parallel dispatch (all Agent() calls in one message), capture the returned IDs from the batch response — one entry per issue — before entering Step 4B's monitoring loop. Without this capture, `resume=` calls in Steps 4B and 4B.5 will have no agent ID to reference and the resume will fail. <!-- Added: forge#1083 -->
|
|
333
570
|
|
|
334
|
-
**Launch
|
|
571
|
+
**Launch only up to the concurrency cap** (forge#1912) — never put more than `dispatch_headroom` (`MAX_CONCURRENT - ACTIVE_DISPATCH_COUNT`, from Step 4A-pre.0.2) Agent tool calls into a single message. Use `run_in_background=true` so the dispatched agents execute in parallel with each other, within the cap.
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
# Compute this dispatch batch (forge#1912): merge any previously-deferred concurrency
|
|
575
|
+
# issues (oldest first, for fairness) with the current ready set, then take only as many
|
|
576
|
+
# as headroom allows. The remainder is re-queued, not dropped.
|
|
577
|
+
HEADROOM=$(dispatch_headroom)
|
|
578
|
+
CANDIDATES=("${DEFERRED_CONCURRENCY_ISSUES[@]}" {ready_issue_numbers})
|
|
579
|
+
DEFERRED_CONCURRENCY_ISSUES=()
|
|
580
|
+
|
|
581
|
+
DISPATCH_NOW=()
|
|
582
|
+
for NUM in "${CANDIDATES[@]}"; do
|
|
583
|
+
if [ "${#DISPATCH_NOW[@]}" -lt "$HEADROOM" ]; then
|
|
584
|
+
DISPATCH_NOW+=("$NUM")
|
|
585
|
+
else
|
|
586
|
+
DEFERRED_CONCURRENCY_ISSUES+=("$NUM")
|
|
587
|
+
fi
|
|
588
|
+
done
|
|
589
|
+
|
|
590
|
+
if [ "${#DEFERRED_CONCURRENCY_ISSUES[@]}" -gt 0 ]; then
|
|
591
|
+
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[*]}"
|
|
592
|
+
fi
|
|
593
|
+
echo "Dispatching ${#DISPATCH_NOW[@]} issue(s) this message (headroom was ${HEADROOM})"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
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 `${#DISPATCH_NOW[@]}` — this happens in addition to, not instead of, capturing each returned agent ID into `AGENT_ISSUE_MAP` above.
|
|
597
|
+
|
|
598
|
+
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).
|
|
335
599
|
|
|
336
600
|
### Step 4B: Monitor completions and dispatch newly ready issues
|
|
337
601
|
|
|
338
602
|
You will be automatically notified when each background agent completes. **Do NOT use `sleep` loops to poll for completion.** Instead, wait for the automatic notification. When you receive a notification that an agent completed, immediately process it.
|
|
339
603
|
|
|
604
|
+
**Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant an `agent_completed` notification arrives, 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 (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=...)` call is issued — a resume re-occupies a worker slot exactly like a fresh dispatch does.
|
|
605
|
+
|
|
606
|
+
**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.
|
|
607
|
+
|
|
340
608
|
**Successor dispatch latency is measured from `agent_completed`, not from orchestrator polling.** The moment you receive an `agent_completed` notification for issue N, that is t=0 for dispatching N's successors. Any successor whose predecessors are all now terminal MUST be dispatched in the same response that processes the notification — not after a poll cycle, not after a sleep. This is the design property that makes streaming DAG execution faster than wave-based execution. <!-- Added: forge#1251 -->
|
|
341
609
|
|
|
342
|
-
**
|
|
610
|
+
**Predecessor Classification (DONE / GATED / FAILED)** <!-- Added: forge#1812 --> — every check in this file that asks "is predecessor X resolved enough for its dependents to proceed" MUST classify X into exactly one of three states below — never a single binary terminal/non-terminal grep. Earlier versions of this file independently patched `grep -qE 'workflow:merged|workflow:invalid|needs-human|CLOSED'` in multiple places, and the copies drifted: the readiness check (this step) treated `needs-human` as done-enough-to-dispatch-through, while the failure handler (item 6 below) treated the identical label as a hard failure that skips dependents. Both cannot be right at once — `needs-human` means the predecessor's code is paused pending a human decision and is NOT yet in the base branch, so dispatching a dependent against it is unsafe, but permanently skipping the dependent is also wrong once the human resolves the block. The fix is a third state:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
classify_predecessor_state() {
|
|
614
|
+
local PRED="$1"
|
|
615
|
+
local PRED_INFO
|
|
616
|
+
# NOTE: the `workflow` array below deliberately also keeps `needs-human` — that label has NO
|
|
617
|
+
# `workflow:` prefix (see bin/labels.json), so a bare `select(startswith("workflow:"))` would
|
|
618
|
+
# drop it and the GATED branch's `needs-human` case would be dead code (forge#1812 primary case).
|
|
619
|
+
PRED_INFO=$(gh issue view "$PRED" -R {GH_REPO} --json labels,state \
|
|
620
|
+
--jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:") or . == "needs-human")]}' 2>/dev/null || echo '{}')
|
|
621
|
+
local PRED_STATE PRED_LABELS
|
|
622
|
+
PRED_STATE=$(echo "$PRED_INFO" | jq -r '.state // "OPEN"')
|
|
623
|
+
PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
|
|
624
|
+
|
|
625
|
+
if echo "$PRED_LABELS" | grep -qx "workflow:invalid"; then
|
|
626
|
+
echo "FAILED"
|
|
627
|
+
elif echo "$PRED_LABELS" | grep -qx "workflow:merged"; then
|
|
628
|
+
echo "DONE"
|
|
629
|
+
elif echo "$PRED_LABELS" | grep -qxE "needs-human|workflow:awaiting-merge"; then
|
|
630
|
+
echo "GATED"
|
|
631
|
+
elif [ "$PRED_STATE" = "CLOSED" ]; then
|
|
632
|
+
# Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
|
|
633
|
+
# not a new deadlock state; there is no pending code for dependents to wait on.
|
|
634
|
+
echo "DONE"
|
|
635
|
+
else
|
|
636
|
+
echo "IN_PROGRESS"
|
|
637
|
+
fi
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
- **DONE** — predecessor's code is in the base branch (`workflow:merged`), or the predecessor is closed with no pending code. Safe for dependents to dispatch.
|
|
642
|
+
- **GATED** — predecessor is paused pending a human decision (`needs-human`) or pending only a human merge click (`workflow:awaiting-merge`). Its code is NOT yet in the base branch. Dependents are neither dispatched nor skipped — they move to the `blocked-on-human-merge` tracked state (item 6.5 below).
|
|
643
|
+
- **FAILED** — predecessor was closed as `workflow:invalid`, or the agent explicitly reported a build/test error. Dependents are marked "skipped — dependency failed" (item 6 below) — unchanged from prior behavior.
|
|
644
|
+
- **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`). Dependent simply continues waiting; no special tracking needed.
|
|
645
|
+
|
|
646
|
+
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.
|
|
647
|
+
|
|
648
|
+
**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 or GATED, 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. 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.
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
verify_file_overlap_edge() {
|
|
652
|
+
local PRED="$1"
|
|
653
|
+
local DEP="$2"
|
|
654
|
+
local EDGE_TYPE="${EDGE_KIND["${PRED}:${DEP}"]:-}"
|
|
655
|
+
|
|
656
|
+
# Only Layer 1/2/3 structural edges (same-file / directory / shared-module, tagged in
|
|
657
|
+
# phase-3-dependency.md Step 3C) are eligible for this check. Explicit `Depends on`, the
|
|
658
|
+
# DATABASE domain chain, Layer 4 conservative-fallback edges, and Layer 5 co-change edges
|
|
659
|
+
# never populate EDGE_KIND for a pair — they encode a declared or historical dependency,
|
|
660
|
+
# not a guessed file list, and are NEVER dropped here.
|
|
661
|
+
if [ -z "$EDGE_TYPE" ]; then
|
|
662
|
+
echo "KEEP"
|
|
663
|
+
return
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
# Resolve PRED's PR using the same anchored search already used elsewhere in this file
|
|
667
|
+
# (forge#1634/#1646/#1830 precedent — a bare "#${PRED}" substring grep would false-match
|
|
668
|
+
# #50/#500 for predecessor #5, so this always goes through gh's search query, never a
|
|
669
|
+
# hand-rolled grep against comment/PR body text).
|
|
670
|
+
#
|
|
671
|
+
# Exclude CLOSED-unmerged PRs (same precedent as the milestone-base-consistency check
|
|
672
|
+
# elsewhere in this file: "a closed-but-not-merged PR is a superseded/abandoned routing
|
|
673
|
+
# attempt and does NOT reflect the live lane"). For a FAILED (workflow:invalid) or GATED
|
|
674
|
+
# (needs-human/workflow:awaiting-merge) predecessor this can only ever surface an OPEN PR —
|
|
675
|
+
# a MERGED PR would already classify the predecessor DONE, not FAILED/GATED — so this filter
|
|
676
|
+
# also guarantees `gh pr diff` below targets a live, still-open branch rather than an
|
|
677
|
+
# abandoned PR whose branch may since have been force-deleted (e.g. by /cleanup).
|
|
678
|
+
local PRED_PR
|
|
679
|
+
PRED_PR=$(gh pr list -R {GH_REPO} --state all --search "\"Closes #${PRED}\" in:body" \
|
|
680
|
+
--json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null || echo "")
|
|
681
|
+
|
|
682
|
+
if [ -z "$PRED_PR" ]; then
|
|
683
|
+
# Predecessor reached a terminal/gated state having never opened a PR (or only ever had
|
|
684
|
+
# an abandoned/closed-unmerged one) — there is no live code that could possibly conflict
|
|
685
|
+
# with DEP's files. The edge was based purely on the pre-build guess and never materialized.
|
|
686
|
+
echo "DROP"
|
|
687
|
+
return
|
|
688
|
+
fi
|
|
689
|
+
|
|
690
|
+
# A PR exists — compare the file(s) that actually triggered this edge (EDGE_FILES, set at
|
|
691
|
+
# Layer 1/2/3 in phase-3-dependency.md) against the PR's real changed-file list.
|
|
692
|
+
local ACTUAL_FILES
|
|
693
|
+
local DIFF_EXIT
|
|
694
|
+
ACTUAL_FILES=$(gh pr diff "$PRED_PR" -R {GH_REPO} --name-only 2>/dev/null)
|
|
695
|
+
DIFF_EXIT=$?
|
|
696
|
+
if [ "$DIFF_EXIT" -ne 0 ]; then
|
|
697
|
+
# Fail-safe: could not fetch the actual diff (transient API error, rate limit, etc.) —
|
|
698
|
+
# an empty result here must NOT be conflated with "confirmed no overlap." Keep the edge
|
|
699
|
+
# blocking rather than risk dropping a real conflict on a fetch failure.
|
|
700
|
+
echo "KEEP"
|
|
701
|
+
return
|
|
702
|
+
fi
|
|
703
|
+
local EDGE_FILE_LIST="${EDGE_FILES["${PRED}:${DEP}"]:-}"
|
|
704
|
+
|
|
705
|
+
local OVERLAP_FOUND=false
|
|
706
|
+
for EF in $EDGE_FILE_LIST; do
|
|
707
|
+
[ -z "$EF" ] && continue
|
|
708
|
+
# Strip a trailing slash (Layer 2 directory-type edges store a bare directory path) and
|
|
709
|
+
# escape regex metacharacters, then anchor the match to a path-component boundary. A raw
|
|
710
|
+
# `grep -qF` substring test would let a short directory name like "utils" false-match an
|
|
711
|
+
# unrelated path such as "some_utils_helper.py" or "shared_utils/" — anchoring on `/`
|
|
712
|
+
# boundaries requires the shared file/directory to appear as a whole path segment.
|
|
713
|
+
EF_CLEAN="${EF%/}"
|
|
714
|
+
EF_ESCAPED=$(printf '%s' "$EF_CLEAN" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
715
|
+
if echo "$ACTUAL_FILES" | grep -qE "(^|/)${EF_ESCAPED}(/|$)"; then
|
|
716
|
+
OVERLAP_FOUND=true
|
|
717
|
+
break
|
|
718
|
+
fi
|
|
719
|
+
done
|
|
720
|
+
|
|
721
|
+
if [ "$OVERLAP_FOUND" = "true" ]; then
|
|
722
|
+
echo "KEEP" # PR's actual diff confirms the shared file was really touched — real conflict
|
|
723
|
+
else
|
|
724
|
+
echo "DROP" # PR exists but its actual diff never touched the guessed shared file(s) —
|
|
725
|
+
# the Layer 1/2/3 guess was wrong for this predecessor; the edge is spurious
|
|
726
|
+
fi
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Call sites**: item 6 (FAILED handling, below) and item 6.5 (GATED handling, below) both 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.
|
|
731
|
+
|
|
732
|
+
**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.
|
|
733
|
+
|
|
734
|
+
**Never called from**: the DONE branch (that predecessor's code is already merged — the edge, if real, is already resolved) or the `IN_PROGRESS` branch (nothing to re-verify yet — the predecessor hasn't concluded).
|
|
735
|
+
|
|
736
|
+
**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.
|
|
343
737
|
|
|
344
738
|
```bash
|
|
345
739
|
# After each agent completion, check for newly ready issues:
|
|
346
740
|
for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
347
741
|
ALL_PREDS_DONE=true
|
|
742
|
+
ANY_PRED_GATED=false
|
|
743
|
+
GATING_PREDS=()
|
|
348
744
|
for PRED in {predecessors_of_BLOCKED_NUM}; do
|
|
349
|
-
PRED_STATE=$(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
745
|
+
PRED_STATE=$(classify_predecessor_state "$PRED")
|
|
746
|
+
case "$PRED_STATE" in
|
|
747
|
+
DONE)
|
|
748
|
+
# Same-file current-state brief forwarding (forge#1860): only fires when the
|
|
749
|
+
# RESOLVED edge from this specific PRED to this specific BLOCKED_NUM is a
|
|
750
|
+
# Layer 1/2/3 structural edge (EDGE_KIND set to same-file/directory/shared-module
|
|
751
|
+
# in phase-3-dependency.md Step 3C) — never for explicit `Depends on`, the
|
|
752
|
+
# DATABASE domain chain, Layer 4 conservative-fallback, or Layer 5 co-change edges,
|
|
753
|
+
# since those never populate EDGE_KIND for this pair.
|
|
754
|
+
EDGE_TYPE="${EDGE_KIND["${PRED}:${BLOCKED_NUM}"]:-}"
|
|
755
|
+
if [ -n "$EDGE_TYPE" ] && [ -z "${EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]:-}" ]; then
|
|
756
|
+
EDGE_BRIEFED["${PRED}:${BLOCKED_NUM}"]=1
|
|
757
|
+
SHARED_FILE="${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-}"
|
|
758
|
+
|
|
759
|
+
# Prefer FORGE:BUILDER (the concrete "what changed" record). Fall back to
|
|
760
|
+
# FORGE:ARCHITECT, then FORGE:INVESTIGATOR, for predecessors that never reached
|
|
761
|
+
# implementation (e.g. closed workflow:invalid, or resolved via Phase 2 decomposition).
|
|
762
|
+
BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
|
|
763
|
+
--jq '[.[] | select(.body | contains("FORGE:BUILDER"))] | last | .body // ""' 2>/dev/null)
|
|
764
|
+
BRIEF_SRC_LABEL="FORGE:BUILDER"
|
|
765
|
+
if [ -z "$BRIEF_SRC" ]; then
|
|
766
|
+
BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
|
|
767
|
+
--jq '[.[] | select(.body | contains("FORGE:ARCHITECT"))] | last | .body // ""' 2>/dev/null)
|
|
768
|
+
BRIEF_SRC_LABEL="FORGE:ARCHITECT"
|
|
769
|
+
fi
|
|
770
|
+
if [ -z "$BRIEF_SRC" ]; then
|
|
771
|
+
BRIEF_SRC=$(gh api repos/{GH_REPO}/issues/${PRED}/comments \
|
|
772
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null)
|
|
773
|
+
BRIEF_SRC_LABEL="FORGE:INVESTIGATOR"
|
|
774
|
+
fi
|
|
775
|
+
|
|
776
|
+
if [ -n "$BRIEF_SRC" ]; then
|
|
777
|
+
# Scope the excerpt to the shared file — a few lines, never a raw diff or
|
|
778
|
+
# full comment dump. Fall back to the comment's Approach/Changes section if
|
|
779
|
+
# the file's basename doesn't literally appear (e.g. a directory-level edge).
|
|
780
|
+
FIRST_SHARED_FILE=$(echo "$SHARED_FILE" | awk '{print $1}')
|
|
781
|
+
EXCERPT=""
|
|
782
|
+
if [ -n "$FIRST_SHARED_FILE" ]; then
|
|
783
|
+
EXCERPT=$(echo "$BRIEF_SRC" | grep -iF "$(basename "$FIRST_SHARED_FILE" 2>/dev/null)" | head -3)
|
|
784
|
+
fi
|
|
785
|
+
[ -z "$EXCERPT" ] && EXCERPT=$(echo "$BRIEF_SRC" | sed -n '/### Approach/,/^### /p' | head -6)
|
|
786
|
+
SAME_FILE_BRIEF["$BLOCKED_NUM"]="${SAME_FILE_BRIEF["$BLOCKED_NUM"]:-}
|
|
787
|
+
- **#${PRED}** (${EDGE_TYPE} edge, from its ${BRIEF_SRC_LABEL} comment, \`${SHARED_FILE:-shared file}\`): ${EXCERPT:-see #${PRED}'s ${BRIEF_SRC_LABEL} comment for details}"
|
|
788
|
+
fi
|
|
789
|
+
fi
|
|
790
|
+
;; # satisfied — no dispatch-readiness action beyond the brief above
|
|
791
|
+
FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
|
|
792
|
+
GATED)
|
|
793
|
+
ALL_PREDS_DONE=false
|
|
794
|
+
ANY_PRED_GATED=true
|
|
795
|
+
GATING_PREDS+=("$PRED")
|
|
796
|
+
;; # handled by item 6.5 (blocked-on-human-merge)
|
|
797
|
+
IN_PROGRESS|*) ALL_PREDS_DONE=false ;; # just keep waiting
|
|
798
|
+
esac
|
|
355
799
|
done
|
|
356
800
|
if [ "$ALL_PREDS_DONE" = "true" ]; then
|
|
357
|
-
echo "#{BLOCKED_NUM} is now READY — all predecessors resolved. Dispatching."
|
|
801
|
+
echo "#{BLOCKED_NUM} is now READY — all predecessors DONE (merged/resolved). Dispatching."
|
|
358
802
|
# Add to dispatch batch for this completion cycle
|
|
803
|
+
elif [ "$ANY_PRED_GATED" = "true" ]; then
|
|
804
|
+
echo "#{BLOCKED_NUM} is BLOCKED-ON-HUMAN-MERGE — gated by: ${GATING_PREDS[*]}. See item 6.5."
|
|
805
|
+
# Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
|
|
359
806
|
fi
|
|
360
807
|
done
|
|
361
|
-
# Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues
|
|
808
|
+
# Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues. Step 4A's own dispatch-batch
|
|
809
|
+
# computation (the HEADROOM/DISPATCH_NOW logic, forge#1912) is what actually caps this —
|
|
810
|
+
# "newly ready" here just means "eligible," not "guaranteed to dispatch this cycle." Any
|
|
811
|
+
# issue that doesn't fit current headroom lands in DEFERRED_CONCURRENCY_ISSUES and is
|
|
812
|
+
# retried automatically on the next completion, same as every other concurrency-deferred issue.
|
|
362
813
|
```
|
|
363
814
|
|
|
364
815
|
**CRITICAL — Stall detection and recovery**: Background agents sometimes stop mid-pipeline (`stop_reason=end_turn`) after completing a sub-phase (e.g., investigation completes but build never starts). This causes the agent to "complete" from the Agent tool's perspective even though the `/work-on` pipeline is only partially done. When you receive a completion notification:
|
|
365
816
|
|
|
366
817
|
1. **Check if the agent completed the FULL pipeline** — not just one phase:
|
|
367
818
|
```bash
|
|
368
|
-
# Check final workflow state —
|
|
819
|
+
# Check final workflow state — workflow:merged, workflow:invalid, needs-human, and
|
|
820
|
+
# workflow:awaiting-merge all mean this agent's own /work-on run has stopped (the last
|
|
821
|
+
# two are human-gated pauses, not completions). See Predecessor Classification above for
|
|
822
|
+
# how the DAG's readiness/failure logic treats these same labels differently from "this
|
|
823
|
+
# agent is done running."
|
|
369
824
|
FINAL_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels,state --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}')
|
|
370
825
|
echo "#{NUM}: $FINAL_STATE"
|
|
371
826
|
```
|
|
372
827
|
|
|
373
|
-
2. **If the issue is NOT in a terminal state** (`workflow:merged`, `workflow:invalid`,
|
|
828
|
+
2. **If the issue is NOT in a terminal-for-this-agent state** (`workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`), the agent stalled mid-pipeline. **Resume it immediately**:
|
|
374
829
|
```
|
|
375
830
|
Agent(
|
|
376
831
|
resume=AGENT_ISSUE_MAP[{NUMBER}],
|
|
377
832
|
description="Resume #{NUMBER} pipeline",
|
|
378
833
|
run_in_background=true,
|
|
379
|
-
prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER}') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
|
|
834
|
+
prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER} --under-orchestration') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
|
|
380
835
|
)
|
|
381
836
|
```
|
|
382
|
-
**Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes.
|
|
837
|
+
**Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes. Each resume re-occupies a worker slot — increment `ACTIVE_DISPATCH_COUNT` by 1 per agent resumed here (it was already decremented by the "Concurrency slot release" rule above when the stall was first observed as a completion). <!-- Added: forge#1912 -->
|
|
383
838
|
|
|
384
839
|
3. **Track resume cycles per agent.** If an agent has been resumed 2+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
|
|
385
840
|
|
|
386
|
-
4. **
|
|
841
|
+
4. **Post CLAIM_RELEASED on coordination issue** (when `FORGE_COORD_ISSUE` is set): <!-- Added: forge#1736 -->
|
|
842
|
+
```bash
|
|
843
|
+
# After verifying terminal state for issue NUM, release its claim on the coordination issue
|
|
844
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
|
|
845
|
+
COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
|
|
846
|
+
if [ -n "$COORD_NUM" ]; then
|
|
847
|
+
gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM_RELEASED -->
|
|
848
|
+
**Holder**: #${NUM} — reached terminal state: ${FINAL_WORKFLOW_STATE}
|
|
849
|
+
**Released**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
|
|
850
|
+
echo "CLAIM_RELEASED posted for #${NUM} on coordination issue #${COORD_NUM}"
|
|
851
|
+
fi
|
|
852
|
+
fi
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
**Claims-board relaxation sweep** (run after posting CLAIM_RELEASED): When a claim is released, check all remaining Layer-2/4-serialized issue pairs. If the now-released Holder's claimed files were the *only* conflict reason for a still-blocked issue, and that blocked issue already has an active `FORGE:CLAIM` with a disjoint file set, the blocking edge MAY be relaxed (blocked issue becomes ready). <!-- Added: forge#1736 -->
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
# After CLAIM_RELEASED for issue NUM:
|
|
859
|
+
# Read all active FORGE:CLAIM annotations from coordination issue
|
|
860
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
|
|
861
|
+
ACTIVE_CLAIMS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
|
|
862
|
+
--jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
|
|
863
|
+
select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
|
|
864
|
+
map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
|
|
865
|
+
files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
|
|
866
|
+
# For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
|
|
867
|
+
# is disjoint from all remaining active claims. If so, mark it ready.
|
|
868
|
+
# (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
|
|
869
|
+
for BLOCKED_NUM in {layer_2_4_blocked_issues}; do
|
|
870
|
+
BLOCKED_CLAIM_FILES=$(echo "$ACTIVE_CLAIMS" \
|
|
871
|
+
| jq -r --arg h "#${BLOCKED_NUM}" '.[] | select(.holder | startswith($h)) | .files' 2>/dev/null || echo "")
|
|
872
|
+
if [ -n "$BLOCKED_CLAIM_FILES" ]; then
|
|
873
|
+
# Compare file set with all other active claims — if disjoint, downgrade to ready
|
|
874
|
+
echo "Claims-board relaxation: checking if #${BLOCKED_NUM} can be unblocked based on disjoint claims"
|
|
875
|
+
# (Implementer: build a set-intersection check here using sorted file lists)
|
|
876
|
+
fi
|
|
877
|
+
done
|
|
878
|
+
fi
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
5. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), Awaiting-merge (`workflow:awaiting-merge` — remediated + re-reviewed to APPROVED after an earlier `needs-human` escalation; needs only a human merge, not diagnosis — keep distinct from Blocked in any status output, see item 8), or Error
|
|
882
|
+
|
|
883
|
+
5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready, run Steps 4A.pre.0 → 4A.pre → 4A for them. **Step 4A's own headroom computation is the cap** (forge#1912) — "batch all newly ready issues into a single dispatch message" means one message covers `DISPATCH_NOW` (ready issues that fit current headroom), not necessarily every newly-ready issue; any that don't fit go to `DEFERRED_CONCURRENCY_ISSUES` and retry on the next completion.
|
|
884
|
+
|
|
885
|
+
6. **Handle predecessor failures** — if a completed agent's issue classifies as `FAILED` (`workflow:invalid`, or an explicit build/test error — see Predecessor Classification above; `needs-human` and `workflow:awaiting-merge` are GATED, not FAILED — see item 6.5), check for dependent issues in the DAG.
|
|
886
|
+
|
|
887
|
+
**Edge re-verification before cascading the skip** <!-- Added: forge#1904 -->: For each direct dependent `DEP` of the FAILED predecessor `PRED`, call `verify_file_overlap_edge "$PRED" "$DEP"` (defined above, alongside `classify_predecessor_state`). If the pair has no `EDGE_KIND` entry (explicit dependency, DATABASE chain, Layer 4/5 edge), the helper returns `KEEP` immediately and behavior is unchanged — cascade the skip as before. If the pair IS an `EDGE_KIND` edge (Layer 1/2/3) and the helper returns `DROP` (FAILED predecessor never opened a PR, or its PR's actual diff never touched the guessed shared file), do NOT cascade the skip through that specific predecessor for that specific dependent — remove `PRED` from `DEP`'s predecessor set instead. If `DEP` has other still-unresolved predecessors it continues waiting on them normally; if `PRED` was `DEP`'s only predecessor, `DEP` becomes immediately ready (run Steps 4A.pre.0 → 4A.pre → 4A for it this cycle). Only when the helper returns `KEEP` (or there is no `EDGE_KIND` entry) does the "skipped — dependency #{X} failed" outcome apply, unchanged from prior behavior.
|
|
888
|
+
|
|
889
|
+
Report every dependent whose skip was avoided this way — e.g. "#{DEP} — predecessor #{PRED} failed but never touched the shared file(s); edge dropped, #{DEP} not skipped." For all remaining dependents (real `EDGE_KIND` overlap confirmed, or a non-`EDGE_KIND` edge type), mark them "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
|
|
890
|
+
|
|
891
|
+
6.4. **Auto-dispatch remediation against a `needs-human`-gated predecessor's own PR** <!-- Added: forge#1813 --> — item 6.5 below tracks the *dependents* of a `GATED` predecessor; this item handles the predecessor's own PR, which item 6.5/6.6 never re-drive on their own. Run this check whenever a completed agent's issue classifies `GATED` **specifically via `needs-human`** — NOT `workflow:awaiting-merge`. That second state already means "remediated and re-reviewed to a clean verdict, just needs a human's merge click" (see forge#1810's guard) — dispatching remediation again would be redundant, not just wasteful, since there is nothing left to fix.
|
|
892
|
+
|
|
893
|
+
```bash
|
|
894
|
+
PRED_CURRENT_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
|
|
895
|
+
--jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // empty' 2>/dev/null)
|
|
896
|
+
|
|
897
|
+
if [ "$PRED_CURRENT_LABEL" = "needs-human" ]; then
|
|
898
|
+
# Resolve PRED's open PR using the anchored search (forge#1634/#1646 precedent —
|
|
899
|
+
# never a bare-number search, which would misattribute an unrelated PR).
|
|
900
|
+
GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
|
|
901
|
+
--json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
902
|
+
|
|
903
|
+
if [ -n "$GATING_PR" ]; then
|
|
904
|
+
# Idempotency guard: only one remediation attempt per PR, ever (single-attempt
|
|
905
|
+
# semantics — remediate.md's own Phase M0 enforces this too, but checking here
|
|
906
|
+
# avoids spawning a redundant agent that would immediately no-op on entry).
|
|
907
|
+
ALREADY_REMEDIATED=$(gh api repos/{GH_REPO}/issues/${GATING_PR}/comments \
|
|
908
|
+
--jq '[.[] | select(.body | contains("FORGE:REMEDIATION"))] | length' 2>/dev/null || echo "0")
|
|
909
|
+
|
|
910
|
+
if [ "$ALREADY_REMEDIATED" -eq 0 ]; then
|
|
911
|
+
echo "Dispatching remediation for #{PRED}'s gating PR #{GATING_PR} (needs-human)"
|
|
912
|
+
# Same Agent-spawn-fallback style as Step 4A's template — one background agent,
|
|
913
|
+
# whose sole job is to invoke /work-on in remediation mode and let it run to
|
|
914
|
+
# completion (AUTO-LANDED, HELD-AWAITING-MERGE, RE-ESCALATED, or UNFIXABLE — all
|
|
915
|
+
# are terminal-for-this-agent; see work-on/remediate.md Output).
|
|
916
|
+
Agent(
|
|
917
|
+
subagent_type="general-purpose",
|
|
918
|
+
model="{SUBAGENT_MODEL}",
|
|
919
|
+
description="Remediate PR #{GATING_PR} (needs-human, blocks #{PRED})",
|
|
920
|
+
run_in_background=true,
|
|
921
|
+
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}.
|
|
922
|
+
|
|
923
|
+
**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.
|
|
924
|
+
|
|
925
|
+
**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`.
|
|
926
|
+
|
|
927
|
+
Do not ask the user questions — you are running autonomously in the background."
|
|
928
|
+
)
|
|
929
|
+
fi
|
|
930
|
+
fi
|
|
931
|
+
fi
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
This satisfies #1809 Q2 (the orchestrator auto-dispatches remediation against the gated predecessor itself — the exact gap forge#1812's item 6.5/6.6 left open, since those items only ever track and wake *dependents*, never the gated PR's own remediation). The remediation agent's outcome is picked up on the **next** completion-monitoring cycle of this same Step 4B loop: if it lands (`workflow:merged`), item 6.6 below fires normally and wakes any `blocked-on-human-merge` dependents; if it holds/re-escalates, the predecessor simply remains `GATED` and item 6.5 continues tracking its dependents unchanged.
|
|
935
|
+
|
|
936
|
+
6.5. **Handle predecessor gating** (`GATED` — `needs-human` or `workflow:awaiting-merge`) <!-- Added: forge#1812 --> — if a completed agent's issue classifies as `GATED`, its direct dependents are neither dispatched nor marked failed/skipped. For each direct dependent `DEP` of the gated predecessor `PRED`:
|
|
937
|
+
|
|
938
|
+
**Edge re-verification gate (run FIRST, before any tracking)** <!-- Added: forge#1904 -->:
|
|
939
|
+
```bash
|
|
940
|
+
EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$DEP")
|
|
941
|
+
if [ "$EDGE_VERDICT" = "DROP" ]; then
|
|
942
|
+
echo "#{DEP} — predecessor #{PRED} is GATED but its EDGE_KIND file-overlap edge does not hold (no PR ever opened, or its actual diff never touched the guessed shared file(s)). Dropping the edge — not tracking #{DEP} as blocked-on-human-merge for this predecessor."
|
|
943
|
+
# Remove PRED from DEP's predecessor set. If DEP has other unresolved predecessors it
|
|
944
|
+
# keeps waiting on those normally. If PRED was DEP's only predecessor, DEP is immediately
|
|
945
|
+
# ready — run Steps 4A.pre.0 → 4A.pre → 4A for it this cycle (same as any other newly
|
|
946
|
+
# ready issue in the core streaming dispatch loop above).
|
|
947
|
+
continue # skip the tracking block below entirely for this (PRED, DEP) pair
|
|
948
|
+
fi
|
|
949
|
+
# EDGE_VERDICT = KEEP (real EDGE_KIND overlap confirmed, or no EDGE_KIND entry at all —
|
|
950
|
+
# explicit dependency / DATABASE chain / Layer 4-5 edges always fall through to KEEP and
|
|
951
|
+
# proceed with tracking exactly as before this fix).
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Existing tracking logic (unchanged for the KEEP case)**:
|
|
955
|
+
```bash
|
|
956
|
+
# Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
|
|
957
|
+
# do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
|
|
958
|
+
GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
|
|
959
|
+
--json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
960
|
+
PRED_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
|
|
961
|
+
--jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // "needs-human"' 2>/dev/null)
|
|
962
|
+
|
|
963
|
+
# Self-heal the label if it hasn't been bootstrapped yet (same pattern as review-pr.md 6C —
|
|
964
|
+
# colors match the canonical manifest bin/labels.json; --force makes this idempotent/cheap).
|
|
965
|
+
gh label create "blocked-on-human-merge" --color "006B75" --description "Dependent of a gated (needs-human/awaiting-merge) predecessor. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
966
|
+
|
|
967
|
+
# Idempotency: only post/label if not already tracked for this specific predecessor.
|
|
968
|
+
# Anchor on the exact "**Gating predecessor**: #N" label with a word boundary —
|
|
969
|
+
# a bare contains("#N") substring would false-match #50/#500 for predecessor #5. <!-- forge#1830 -->
|
|
970
|
+
ALREADY_TRACKED=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
971
|
+
--jq --arg prednum "${PRED}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
|
|
972
|
+
if [ "$ALREADY_TRACKED" -eq 0 ]; then
|
|
973
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
|
|
974
|
+
**Gating predecessor**: #${PRED} (state: \`${PRED_LABEL}\`${GATING_PR:+, open PR #${GATING_PR}})
|
|
975
|
+
**Status**: This issue is ready to dispatch as soon as #${PRED}'s gating PR merges. No action needed — the orchestrator (live session via item 6.6, or the next \`/orchestrate\` invocation via phase-3-dependency.md's wake reconstruction) will auto-dispatch it the moment #${PRED} reaches \`workflow:merged\`."
|
|
976
|
+
gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
|
|
977
|
+
fi
|
|
978
|
+
```
|
|
979
|
+
Do NOT dispatch `DEP`. Do NOT mark it skipped — it remains visibly tracked as `blocked-on-human-merge` in the DAG, re-evaluated on the next completion event, stall-detection pass, or session wake.
|
|
980
|
+
|
|
981
|
+
6.6. **Merge-triggered wake for blocked-on-human-merge dependents** <!-- Added: forge#1812 --> — whenever a completed agent's issue classifies as `DONE` via `workflow:merged` (i.e. it just merged), check whether any other issue is tracked as blocked on it — this makes gated dependents dispatch the instant the gating PR merges, with no manual `/orchestrate` re-run required:
|
|
982
|
+
```bash
|
|
983
|
+
WOKEN=$(gh issue list -R {GH_REPO} --state open --label "blocked-on-human-merge" --json number \
|
|
984
|
+
--jq '.[].number' 2>/dev/null || echo "")
|
|
985
|
+
for DEP in $WOKEN; do
|
|
986
|
+
IS_GATED_BY_THIS=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
987
|
+
--jq --arg prednum "${NUM}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
|
|
988
|
+
[ "$IS_GATED_BY_THIS" -gt 0 ] || continue
|
|
989
|
+
# Idempotency: only dispatch if DEP hasn't already been dispatched by another path.
|
|
990
|
+
DEP_ALREADY_DISPATCHED=$(gh issue view "$DEP" -R {GH_REPO} --json labels \
|
|
991
|
+
--jq '[.labels[].name | select(startswith("workflow:"))] | length' 2>/dev/null || echo "0")
|
|
992
|
+
if [ "$DEP_ALREADY_DISPATCHED" -eq 0 ]; then
|
|
993
|
+
gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
|
|
994
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
|
|
995
|
+
Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was tracked via a prior FORGE:BLOCKED_ON_HUMAN_MERGE comment.)"
|
|
996
|
+
echo "#{DEP} unblocked by #{NUM} merge — dispatching immediately (Steps 4A.pre.0 → 4A.pre → 4A)."
|
|
997
|
+
# Add DEP to the same-response dispatch batch
|
|
998
|
+
fi
|
|
999
|
+
done
|
|
1000
|
+
```
|
|
1001
|
+
This satisfies the live-session case. For the case where the gating PR merges after the orchestrator session has already ended, the equivalent check runs in `phase-3-dependency.md`'s wake/compaction reconstruction on the next `/orchestrate` invocation — see that file's "Orchestrator state reconstruction on wake / after compaction" section.
|
|
1002
|
+
|
|
1003
|
+
6.7. **Human-gated idle/backpressure check** (`BATCH_FULLY_GATED`) <!-- Added: forge#1814 --> — run this after every completion cycle, once the per-issue classification above (items 5-6.6) has been applied for this cycle. It answers a different question than the paused-drain/blocked-on-human-merge tracking above: those items handle *individual* gated predecessors and their *direct* dependents; this check asks whether the **entire original batch** has now exhausted into human-gated states, which is the condition under which continuing to dispatch cascade-spawned review findings (Step 4C) produces net-negative churn — closing 1 issue while opening 2-4 more, with the real blockers (the GATED issues) unresolved:
|
|
1004
|
+
|
|
1005
|
+
```bash
|
|
1006
|
+
# BATCH_FULLY_GATED is computed over {all_batch_issue_numbers} — the ORIGINAL batch issues
|
|
1007
|
+
# this /orchestrate invocation was given, NOT cascade-spawned review-finding issues (those are
|
|
1008
|
+
# a separate, currently-unbounded-looking stream that this check exists to cap). Cascade
|
|
1009
|
+
# findings are excluded here because they are the SYMPTOM (Step 4C keeps producing them);
|
|
1010
|
+
# counting them as "still IN_PROGRESS" would make this check permanently false and defeat
|
|
1011
|
+
# its own purpose.
|
|
1012
|
+
ANY_ORIGINAL_IN_PROGRESS=false
|
|
1013
|
+
ANY_ORIGINAL_GATED=false
|
|
1014
|
+
for ORIG_NUM in {all_batch_issue_numbers}; do
|
|
1015
|
+
ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
|
|
1016
|
+
case "$ORIG_STATE" in
|
|
1017
|
+
IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
|
|
1018
|
+
GATED) ANY_ORIGINAL_GATED=true ;;
|
|
1019
|
+
DONE|FAILED) ;; # exhausted — no action
|
|
1020
|
+
esac
|
|
1021
|
+
done
|
|
1022
|
+
|
|
1023
|
+
if [ "$ANY_ORIGINAL_IN_PROGRESS" = "false" ] && [ "$ANY_ORIGINAL_GATED" = "true" ]; then
|
|
1024
|
+
BATCH_FULLY_GATED=true
|
|
1025
|
+
else
|
|
1026
|
+
BATCH_FULLY_GATED=false
|
|
1027
|
+
fi
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
- **`BATCH_FULLY_GATED=true`** requires BOTH: no original-batch issue is still `IN_PROGRESS` (i.e. nothing from the original scope will complete on its own without a human), AND at least one original-batch issue is `GATED` (`needs-human`/`workflow:awaiting-merge`, or a dependent already tracked `blocked-on-human-merge`). A batch that finishes entirely `DONE`/`FAILED` with zero `GATED` issues is NOT idle — it is simply complete; do not confuse the two.
|
|
1031
|
+
- **This is a live, recomputed flag, not a one-way latch.** Re-run it every completion cycle. If a gating PR merges (item 6.6 fires) and unblocks an original-batch dependent that becomes dispatchable again, `ANY_ORIGINAL_IN_PROGRESS` flips back to `true` on the next cycle and `BATCH_FULLY_GATED` flips back to `false` — normal dispatch resumes automatically. This is what prevents a permanent idle state and satisfies "no regression: when productive non-gated work remains, the orchestrator continues normally."
|
|
1032
|
+
- **Effect when true**: Step 4C's cascade-finding dispatch (the "For queued (non-deferred) findings" block) is suppressed — see the `BATCH_FULLY_GATED` check added there. The first time the flag flips from `false`/unset to `true` in a completion cycle, print the idle report below and stop actively dispatching new cascade work; the batch remains resumable exactly as item 6.6 and `phase-3-dependency.md`'s wake reconstruction already guarantee.
|
|
1033
|
+
|
|
1034
|
+
**Idle report** (print once, the cycle `BATCH_FULLY_GATED` first becomes true):
|
|
1035
|
+
```
|
|
1036
|
+
⏸ Orchestrator Idle — Waiting on N Merge(s)
|
|
1037
|
+
|
|
1038
|
+
The remaining batch is fully human-gated: every original issue is either merged/invalid, or
|
|
1039
|
+
blocked on a human decision/merge. No further autonomous progress is possible until one of the
|
|
1040
|
+
PRs below is merged. Newly-spawned review-finding issues are being deferred (not dispatched) so
|
|
1041
|
+
the open-issue count does not inflate while nothing productive can close.
|
|
387
1042
|
|
|
388
|
-
|
|
1043
|
+
{reuse the Merge-Ready table computation from phase-6-report.md Step 6A.5 (MERGE_READY_PRS) and
|
|
1044
|
+
the Blocked-on-Merge table from Step 6A.6 (BLOCKED_ON_MERGE) — both already anchor their PR
|
|
1045
|
+
lookups on "Closes #N" in:body per forge#1634/#1646/#1822, so this reuses that logic verbatim
|
|
1046
|
+
rather than re-implementing a parallel PR-resolution path}
|
|
389
1047
|
|
|
390
|
-
|
|
1048
|
+
Findings deferred (idle policy): {count of newly-queued findings deferred this cycle}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
This report is an interim, in-progress print — it does NOT replace the final consolidated report from Phase 6, which runs once the session actually ends or the next `/orchestrate` invocation picks the batch back up; see `phase-6-report.md` Step 6B for the corresponding "Orchestration Paused — Idle" header.
|
|
391
1052
|
|
|
392
1053
|
7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
|
|
393
1054
|
```bash
|
|
@@ -399,18 +1060,44 @@ done
|
|
|
399
1060
|
```
|
|
400
1061
|
If an agent bypassed the pipeline, report it as a **failure** regardless of whether a PR exists.
|
|
401
1062
|
|
|
402
|
-
8. **Post a
|
|
1063
|
+
8. **Post a status update** to the user after each agent reaches terminal state. Format is gated by `{NARRATION_MODE}` (resolved in `config.md` from `pipeline.narration`, default `"terse"`):
|
|
1064
|
+
|
|
1065
|
+
**`terse` (default)** — one line per completion, no table, nothing repeated between completions:
|
|
403
1066
|
```
|
|
404
1067
|
✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
|
|
405
1068
|
✗ #{NUMBER} — {title} → {reason for failure}
|
|
406
1069
|
⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
|
|
1070
|
+
⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved — human merge only, no diagnosis needed)
|
|
1071
|
+
🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
|
|
407
1072
|
⏳ Progress: {completed}/{total} complete, {active} active, {blocked} blocked
|
|
408
1073
|
→ Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
|
|
409
1074
|
```
|
|
410
1075
|
|
|
1076
|
+
**`verbose`** — same one-liners, plus a running per-completion recap table (title, PR, target, elapsed):
|
|
1077
|
+
```
|
|
1078
|
+
✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
|
|
1079
|
+
|
|
1080
|
+
| # | Title | PR | Target | Elapsed |
|
|
1081
|
+
|---|-------|----|--------|---------|
|
|
1082
|
+
{one row per issue completed so far in this batch}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
**This gate is cosmetic only** — it changes what prints to the terminal between completions, never which phases run or what gets committed. Step 6B's full tables (Implementation Results, Review-Spawned Issues, Batch Trajectory Analytics) always render once at the end regardless of `{NARRATION_MODE}`; terse mode just skips the redundant per-completion recap, since Step 6B already aggregates everything.
|
|
1086
|
+
|
|
1087
|
+
`⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
|
|
1088
|
+
two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
|
|
1089
|
+
`⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
|
|
1090
|
+
forge#1812) is distinct from both: it marks a DEPENDENT of a GATED predecessor (item 6.5) — the
|
|
1091
|
+
dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
|
|
1092
|
+
"Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
|
|
1093
|
+
|
|
411
1094
|
9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
|
|
412
1095
|
|
|
413
|
-
**Termination condition**: All issues in the DAG
|
|
1096
|
+
**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 → proceed to Phase 5.
|
|
1097
|
+
|
|
1098
|
+
**Relationship to `DEFERRED_CONCURRENCY_ISSUES` (forge#1912)**: A non-empty `DEFERRED_CONCURRENCY_ISSUES[]` never satisfies either termination condition above — it means dispatchable work exists but is temporarily held back by the concurrency cap, not that the DAG has stalled. Unlike `blocked-on-human-merge`, this is never reported as a "paused drain" requiring human action — it self-resolves automatically the moment any in-flight agent completes and frees a slot (Step 4A's `dispatch_headroom` recomputes every cycle). Only treat the DAG as drained once `DEFERRED_CONCURRENCY_ISSUES` is also empty.
|
|
1099
|
+
|
|
1100
|
+
**Relationship to `BATCH_FULLY_GATED` (item 6.7, forge#1814)**: A paused drain (above) describes the *original batch DAG* reaching a stable, non-progressing state. `BATCH_FULLY_GATED` is the mechanism that keeps that stable state from being masked by cascade churn — without it, Step 4C would keep dispatching new review-finding issues indefinitely (each with its own predecessors/dependents), so the DAG would never actually look "drained" even though the original batch's productive work stopped the moment the last non-gated issue completed. Once `BATCH_FULLY_GATED` is true, Step 4C stops adding new dispatchable work (rule 0 defers all newly-spawned findings), which lets the batch actually reach the paused-drain termination condition above instead of chasing an ever-growing cascade tail. Report this state using the idle report from item 6.7, not a plain "waiting for agents" message — the whole point is to make the pause visible and actionable (which PR(s) to merge), not silent.
|
|
414
1101
|
|
|
415
1102
|
**Anti-pattern — DO NOT DO THIS:**
|
|
416
1103
|
- `sleep 60/120/180/300` loops to check status — you will be notified automatically
|
|
@@ -437,9 +1124,12 @@ STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/nul
|
|
|
437
1124
|
**For each non-terminal agent in the current batch**:
|
|
438
1125
|
```bash
|
|
439
1126
|
for NUM in {active_issue_numbers}; do
|
|
440
|
-
# Skip issues already in terminal state
|
|
1127
|
+
# Skip issues already in a terminal-for-this-agent state (merged/invalid/needs-human/awaiting-merge).
|
|
1128
|
+
# workflow:awaiting-merge MUST be included here (forge#1812) — otherwise the stall detector
|
|
1129
|
+
# re-escalates an already-remediated-and-re-approved PR back to needs-human after STALL_TIMEOUT,
|
|
1130
|
+
# silently collapsing the Awaiting-Merge/Blocked distinction forge#1811 introduced.
|
|
441
1131
|
TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
|
|
442
|
-
--jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
|
|
1132
|
+
--jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human" or . == "workflow:awaiting-merge")] | length')
|
|
443
1133
|
[ "$TERMINAL" -gt 0 ] && continue
|
|
444
1134
|
|
|
445
1135
|
# Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
|
|
@@ -528,16 +1218,18 @@ gh issue list -R {GH_REPO} --state open --label "review-finding" --limit 20 \
|
|
|
528
1218
|
For each spawned finding, determine whether it should be **executed** or **deferred**:
|
|
529
1219
|
|
|
530
1220
|
**Evaluation order** (first matching rule wins):
|
|
1221
|
+
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.
|
|
531
1222
|
1. **Generation ≥ 2** (always defer, even for P1/P2): Finding was spawned by an issue that was itself a review-finding. Check the source issue's labels for `review-finding` — if the source has that label, the new finding is generation 2. Always defer. Rationale: gen-2+ cascade is theoretically unbounded — cap it here.
|
|
532
1223
|
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.
|
|
533
1224
|
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.
|
|
534
1225
|
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.
|
|
1226
|
+
5. **Per-batch token budget** (P3 and below only, applied AFTER surface-area batching below — see "Per-batch token budget gate") <!-- Added: forge#1858 -->: Once `BATCH_TOKEN_SPEND` would exceed `TOKEN_BUDGET`, additional P3-and-below units (an unclubbed finding, or an already-clubbed batch issue) defer rather than dispatch. This is NOT part of the per-finding rule 0-4 chain immediately below — it is a quantity gate applied to the POST-clubbing `QUEUED_FINDINGS` list, so a same-run surface-area batch issue (surface-area batching below) is charged once for the whole cluster, not once per member. P1/P2 are NEVER gated by it (they are excluded by rule 2 before reaching this gate, same as they are excluded from rules 3-4).
|
|
535
1227
|
|
|
536
|
-
**Defer** (do NOT add to the DAG) if rules 1, 3, or
|
|
1228
|
+
**Defer** (do NOT add to the DAG) if rules 0, 1, 3, 4, or 5 match.
|
|
537
1229
|
|
|
538
1230
|
**Execute** (add to the DAG) if:
|
|
539
|
-
- Rule 2 matches (P1 or P2)
|
|
540
|
-
- None of the defer rules matched (generation 1, P3 with no file overlap, not a keyword match)
|
|
1231
|
+
- Rule 2 matches (P1 or P2) — AND rule 0 did not already match (rule 0 is checked first and overrides rule 2)
|
|
1232
|
+
- None of the defer rules matched (generation 1, P3 with no file overlap, not a keyword match, batch not fully gated) AND rule 5's token budget still has headroom for this unit
|
|
541
1233
|
|
|
542
1234
|
**Before running the loop, build the batch file list (MANDATORY for Heuristic 3):**
|
|
543
1235
|
|
|
@@ -573,8 +1265,13 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
573
1265
|
PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
|
|
574
1266
|
TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
|
|
575
1267
|
|
|
1268
|
+
# Rule 0: Batch fully human-gated — checked FIRST, overrides even the P1/P2 priority
|
|
1269
|
+
# override below. BATCH_FULLY_GATED is computed once per completion cycle in Step 4B
|
|
1270
|
+
# item 6.7; read it here, do not recompute. <!-- Added: forge#1814 -->
|
|
1271
|
+
if [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
|
|
1272
|
+
DEFER=true; DEFER_REASON="batch fully human-gated — idle policy"
|
|
576
1273
|
# Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
|
|
577
|
-
|
|
1274
|
+
elif SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
|
|
578
1275
|
[ -n "$SOURCE_NUM" ] && \
|
|
579
1276
|
gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
|
|
580
1277
|
DEFER=true; DEFER_REASON="generation >= 2 (source #${SOURCE_NUM} is also a review-finding)"
|
|
@@ -607,6 +1304,159 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
607
1304
|
done
|
|
608
1305
|
```
|
|
609
1306
|
|
|
1307
|
+
**Surface-area batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
|
|
1308
|
+
|
|
1309
|
+
Cascade-spawned findings collected within a single `/orchestrate` run never pass back through Phase 1's batching rule — Phase 1 only runs once, at the start. Without a check here, same-file P3 findings spawned mid-run always dispatch individually, defeating the batching policy for exactly the findings it exists to catch. Apply the same grouping rule from `commands/orchestrate/phase-1-resolve.md` ("P3 Review-Finding Batching") to `QUEUED_FINDINGS` before the dispatch step below:
|
|
1310
|
+
|
|
1311
|
+
```bash
|
|
1312
|
+
# Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
|
|
1313
|
+
# exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
|
|
1314
|
+
# with identical patterns — NOT grep ERE — so the two batching checks cannot
|
|
1315
|
+
# classify the same issue body differently. <!-- forge#1837 -->
|
|
1316
|
+
# NOTE: SURFACE_BATCHED_FINDINGS and SURFACE_BATCH_COUNT are declared once in Step 4A.pre
|
|
1317
|
+
# (batch scope) — do NOT re-initialize them here, this block runs per-agent-completion cycle.
|
|
1318
|
+
# SURFACE_FILE_MEMBERS itself must NOT survive across cycles — `declare -A` on an
|
|
1319
|
+
# already-declared associative array is a no-op on its existing contents in bash, so an
|
|
1320
|
+
# explicit `unset` is required to actually clear it before each cycle repopulates it.
|
|
1321
|
+
# <!-- Added: forge#1909 -->
|
|
1322
|
+
unset SURFACE_FILE_MEMBERS
|
|
1323
|
+
declare -A SURFACE_FILE_MEMBERS
|
|
1324
|
+
|
|
1325
|
+
# Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
|
|
1326
|
+
# upstream cascade control; this cap holds even if that bound is later loosened,
|
|
1327
|
+
# so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
|
|
1328
|
+
MAX_BATCH_SCAN=50
|
|
1329
|
+
SCANNED=0
|
|
1330
|
+
|
|
1331
|
+
for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
1332
|
+
SCANNED=$((SCANNED + 1))
|
|
1333
|
+
if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
|
|
1334
|
+
echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
|
|
1335
|
+
break
|
|
1336
|
+
fi
|
|
1337
|
+
|
|
1338
|
+
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
|
|
1339
|
+
--jq '{title: .title, body: .body, labels: [.labels[].name]}')
|
|
1340
|
+
|
|
1341
|
+
# Safety exclusions — never batch, at any priority. Same jq test() engine and
|
|
1342
|
+
# patterns as phase-1-resolve.md's batching rule (single shared mechanism).
|
|
1343
|
+
echo "$FINDING_DATA" | jq -e '
|
|
1344
|
+
(.title | test("security|billing|anti-bot|auth"; "i"))
|
|
1345
|
+
or (.body | test("## Problem[\\s\\S]{0,500}(security|billing|anti-bot|auth)"; "i"))
|
|
1346
|
+
or ([.labels[]] | any(. == "security" or . == "billing" or . == "anti-bot" or . == "auth"))
|
|
1347
|
+
' >/dev/null && continue
|
|
1348
|
+
|
|
1349
|
+
# Only P3 findings are eligible (P1/P2 already dispatched individually above)
|
|
1350
|
+
echo "$FINDING_DATA" | jq -e '[.labels[]] | any(. == "priority:P3")' >/dev/null || continue
|
|
1351
|
+
|
|
1352
|
+
FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
|
|
1353
|
+
[ -z "$FINDING_FILE" ] && continue
|
|
1354
|
+
|
|
1355
|
+
SURFACE_FILE_MEMBERS["$FINDING_FILE"]="${SURFACE_FILE_MEMBERS[$FINDING_FILE]} $FINDING_NUM"
|
|
1356
|
+
done
|
|
1357
|
+
|
|
1358
|
+
# For each same-file cluster of 2+, actually CREATE the batch issue (executable —
|
|
1359
|
+
# mirrors phase-1-resolve.md's "Batch creation rule") and REPLACE the members with
|
|
1360
|
+
# the batch issue in QUEUED_FINDINGS so the dispatch step below never double-dispatches
|
|
1361
|
+
# them. This is what makes SURFACE_BATCHED_FINDINGS a live control, not dead wiring. <!-- forge#1832, forge#1834 -->
|
|
1362
|
+
for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
|
|
1363
|
+
MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
|
|
1364
|
+
[ "${#MEMBERS[@]}" -ge 2 ] || continue
|
|
1365
|
+
|
|
1366
|
+
# Sanitize the affected-file path before interpolating it into the issue title/body.
|
|
1367
|
+
# Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
|
|
1368
|
+
# restrict to a validated charset so the value cannot break the gh argument
|
|
1369
|
+
# boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
|
|
1370
|
+
SAFE_SURFACE_AREA=$(printf '%s' "$FILE" | tr -cd 'A-Za-z0-9._/-')
|
|
1371
|
+
|
|
1372
|
+
echo "Same-run surface-area cluster: ${#MEMBERS[@]} P3 findings share $FILE — creating batch issue(s)"
|
|
1373
|
+
|
|
1374
|
+
# Cap at 8 members per batch (phase-1-resolve.md limit); split into batches of <=8.
|
|
1375
|
+
for START in $(seq 0 8 $(( ${#MEMBERS[@]} - 1 ))); do
|
|
1376
|
+
CHUNK=("${MEMBERS[@]:$START:8}")
|
|
1377
|
+
[ "${#CHUNK[@]}" -ge 2 ] || continue
|
|
1378
|
+
|
|
1379
|
+
MEMBER_LINES=""
|
|
1380
|
+
for M in "${CHUNK[@]}"; do
|
|
1381
|
+
MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
|
|
1382
|
+
MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
|
|
1383
|
+
done
|
|
1384
|
+
|
|
1385
|
+
BATCH_ISSUE_NUM=$(gh issue create {GH_FLAG} \
|
|
1386
|
+
--title "fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (same-run batch)" \
|
|
1387
|
+
--label "review-finding,priority:P3,batch" \
|
|
1388
|
+
--body "$(cat <<BATCH_EOF
|
|
1389
|
+
## Problem
|
|
1390
|
+
|
|
1391
|
+
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.
|
|
1392
|
+
|
|
1393
|
+
## Member Findings
|
|
1394
|
+
|
|
1395
|
+
<!-- FORGE:BATCH_MEMBERS -->
|
|
1396
|
+
${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
|
|
1397
|
+
|
|
1398
|
+
## Acceptance Criteria
|
|
1399
|
+
|
|
1400
|
+
- [ ] All member findings addressed or closed as false-positive
|
|
1401
|
+
- [ ] Member issues auto-closed with reference to this batch PR on merge
|
|
1402
|
+
- [ ] No security, billing, anti-bot, or auth paths touched (validated before batching)
|
|
1403
|
+
|
|
1404
|
+
<!-- FORGE:BATCHABLE -->
|
|
1405
|
+
BATCH_EOF
|
|
1406
|
+
)" --json number --jq '.number')
|
|
1407
|
+
|
|
1408
|
+
# Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
|
|
1409
|
+
# with the single batch issue, so the dispatch step below operates on the
|
|
1410
|
+
# batch unit and skips the individual members.
|
|
1411
|
+
SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
|
|
1412
|
+
[ -n "$BATCH_ISSUE_NUM" ] && SURFACE_BATCH_COUNT=$((SURFACE_BATCH_COUNT + 1)) # forge#1858 Step 6B counter
|
|
1413
|
+
QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
|
|
1414
|
+
[ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
|
|
1415
|
+
echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
|
|
1416
|
+
done
|
|
1417
|
+
done
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
Findings clustered here are replaced by their batch issue in `QUEUED_FINDINGS` (and therefore the DAG, which is built from `QUEUED_FINDINGS` in the dispatch step below) — the individual member issues in `SURFACE_BATCHED_FINDINGS` are never dispatched. Findings that remain ungrouped (fewer than 2 sharing a file in this collection round) stay individually queued below; they retain default-batchable eligibility and will be picked up by the next `/orchestrate` invocation's Phase 1 resolve if a same-file or leaf-directory cluster later forms across runs.
|
|
1421
|
+
|
|
1422
|
+
**Per-batch token budget gate (P3-and-below only — MANDATORY, runs on the POST-clubbing `QUEUED_FINDINGS` list):** <!-- Added: forge#1858 -->
|
|
1423
|
+
|
|
1424
|
+
Rule 5 from the evaluation order above. Charges ONE `TOKEN_ESTIMATE_PER_FINDING` per unit remaining in `QUEUED_FINDINGS` at this point — a same-run surface-area-clubbed batch issue counts as a single unit (its members were already replaced by the batch issue number above), never once per member. `TOKEN_BUDGET`, `TOKEN_ESTIMATE_PER_FINDING`, and `BATCH_TOKEN_SPEND` were declared in Step 4A.pre and persist across every Step 4C run this session performs — do NOT re-initialize `BATCH_TOKEN_SPEND` here. This is a DIFFERENT mechanism from the `--budget N` dollar-cost flag (economic scheduling, forge#1743, `should_dispatch()` in Step 4A-pre.0): that one is opt-in, dollar-denominated, and gates only the *original* `SORTED_READY_SET` dispatch loop — it is never consulted here.
|
|
1425
|
+
|
|
1426
|
+
```bash
|
|
1427
|
+
TOKEN_BUDGET_DEFERRED_THIS_RUN=()
|
|
1428
|
+
|
|
1429
|
+
for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
1430
|
+
FINDING_PRIORITY=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json labels \
|
|
1431
|
+
--jq '([.labels[].name | select(startswith("priority:P"))] | .[0] // "priority:P3") | ltrimstr("priority:")' 2>/dev/null || echo "P3")
|
|
1432
|
+
|
|
1433
|
+
# P1/P2 are NEVER gated by the token budget — they were queued by rule 2 above
|
|
1434
|
+
# regardless of budget headroom; skip them here and leave them in QUEUED_FINDINGS.
|
|
1435
|
+
if [ "$FINDING_PRIORITY" = "P1" ] || [ "$FINDING_PRIORITY" = "P2" ]; then
|
|
1436
|
+
continue
|
|
1437
|
+
fi
|
|
1438
|
+
|
|
1439
|
+
PROJECTED_TOKEN_SPEND=$((BATCH_TOKEN_SPEND + TOKEN_ESTIMATE_PER_FINDING))
|
|
1440
|
+
if [ "$PROJECTED_TOKEN_SPEND" -gt "$TOKEN_BUDGET" ]; then
|
|
1441
|
+
TOKEN_DEFERRED+=("$FINDING_NUM")
|
|
1442
|
+
TOKEN_BUDGET_DEFERRED_THIS_RUN+=("$FINDING_NUM")
|
|
1443
|
+
DEFERRED_REASONS[$FINDING_NUM]="token budget exceeded (est. ${TOKEN_ESTIMATE_PER_FINDING} tokens would push batch spend to ${PROJECTED_TOKEN_SPEND} > ceiling ${TOKEN_BUDGET})"
|
|
1444
|
+
echo "TOKEN BUDGET DEFER: #${FINDING_NUM} (est. ${TOKEN_ESTIMATE_PER_FINDING} tokens, projected total ${PROJECTED_TOKEN_SPEND} > ${TOKEN_BUDGET})"
|
|
1445
|
+
else
|
|
1446
|
+
BATCH_TOKEN_SPEND=$PROJECTED_TOKEN_SPEND
|
|
1447
|
+
fi
|
|
1448
|
+
done
|
|
1449
|
+
|
|
1450
|
+
# Remove budget-deferred units from QUEUED_FINDINGS — they move to DEFERRED_FINDINGS/
|
|
1451
|
+
# TOKEN_DEFERRED instead of dispatching this cycle. Re-evaluable in Step 4F.2.6, never
|
|
1452
|
+
# permanent (distinct from the generation>=2 bucket in Step 4F.1).
|
|
1453
|
+
if [ "${#TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}" -gt 0 ]; then
|
|
1454
|
+
QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}") || true))
|
|
1455
|
+
DEFERRED_FINDINGS+=("${TOKEN_BUDGET_DEFERRED_THIS_RUN[@]}")
|
|
1456
|
+
echo "Token budget: deferred ${#TOKEN_BUDGET_DEFERRED_THIS_RUN[@]} P3-and-below unit(s) this cycle — batch spend now ${BATCH_TOKEN_SPEND}/${TOKEN_BUDGET}"
|
|
1457
|
+
fi
|
|
1458
|
+
```
|
|
1459
|
+
|
|
610
1460
|
**For queued (non-deferred) findings:**
|
|
611
1461
|
|
|
612
1462
|
1. **Add them to the dependency DAG.** They are implementation issues — same as issues spawned by investigations in Phase 2. Compute their predecessor sets using the same conflict detection (Step 3C Layers 1-4) against all remaining blocked/active issues.
|
|
@@ -615,7 +1465,7 @@ done
|
|
|
615
1465
|
```
|
|
616
1466
|
Agent #{COMPLETED} spawned {count} new finding issues: #{A}, #{B}
|
|
617
1467
|
Added to DAG: #{A} (predecessors: {}), #{B} (predecessors: {#{X}})
|
|
618
|
-
Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic)
|
|
1468
|
+
Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic), #{E} (token budget)
|
|
619
1469
|
```
|
|
620
1470
|
4. **Re-run file-overlap detection** (Step 3C) on the expanded issue set — finding issues may conflict with active or queued issues that touch the same files. Ready findings dispatch immediately via the standard Step 4B dispatch loop.
|
|
621
1471
|
|
|
@@ -761,11 +1611,13 @@ fi
|
|
|
761
1611
|
### Step 4E: Handle individual agent failures
|
|
762
1612
|
|
|
763
1613
|
If an agent reports failure or error:
|
|
764
|
-
- **Merge conflict**: Report to user, mark issue as needing human attention
|
|
765
|
-
- **Invalid issue**: Already handled by the agent (closed with comment) — just report it
|
|
766
|
-
- **Build/test failure**: Report the error, suggest manual intervention
|
|
767
|
-
- **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`
|
|
768
|
-
- **Dependency cascade
|
|
1614
|
+
- **Merge conflict**: Report to user, mark issue as needing human attention (`needs-human`). This classifies as **GATED**, not FAILED — see Predecessor Classification in Step 4B. Its dependents follow the Dependency cascade rule below, not a hard skip.
|
|
1615
|
+
- **Invalid issue**: Already handled by the agent (closed with comment) — just report it. This classifies as **FAILED**.
|
|
1616
|
+
- **Build/test failure**: Report the error, suggest manual intervention. This classifies as **FAILED**.
|
|
1617
|
+
- **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`. Not yet terminal — leave dependents in `IN_PROGRESS` wait, no cascade action.
|
|
1618
|
+
- **Dependency cascade** <!-- Updated: forge#1812 -->: Re-run `classify_predecessor_state` (Step 4B) for the failed/gated issue before cascading — do NOT assume every entry above is a hard failure:
|
|
1619
|
+
- If it classifies **FAILED** (invalid, or build/test failure): mark all transitive dependents in the DAG as "skipped — dependency #{X} failed" (same as Step 4B item 6).
|
|
1620
|
+
- If it classifies **GATED** (merge conflict → `needs-human`, or `workflow:awaiting-merge`): do NOT mark dependents skipped. Instead apply Step 4B item 6.5 — track each direct dependent as `blocked-on-human-merge` against this predecessor, so it auto-dispatches via item 6.6 the moment the predecessor reaches `workflow:merged`.
|
|
769
1621
|
|
|
770
1622
|
**Do NOT retry failed agents automatically.** Report the failure and let the user decide.
|
|
771
1623
|
|
|
@@ -775,11 +1627,13 @@ If an agent reports failure or error:
|
|
|
775
1627
|
|
|
776
1628
|
**WHY THIS EXISTS**: Deferred findings accumulate during the batch because of file-overlap and cascade-control heuristics (Step 4C). But once the DAG drains, the conditions that caused deferral often no longer apply — completed issues no longer occupy files, so same-file overlap vanishes. Without this sweep, deferred findings silently pile up across runs and never get resolved.
|
|
777
1629
|
|
|
778
|
-
**Step 4F.1: Classify deferred findings into permanent vs re-evaluable**
|
|
1630
|
+
**Step 4F.1: Classify deferred findings into permanent vs re-evaluable vs idle-gated vs token-gated**
|
|
779
1631
|
|
|
780
1632
|
```bash
|
|
781
1633
|
PERMANENT_DEFERRED=()
|
|
782
1634
|
SWEEP_CANDIDATES=()
|
|
1635
|
+
IDLE_DEFERRED=() # <!-- Added: forge#1814 -->
|
|
1636
|
+
TOKEN_GATED=() # <!-- Added: forge#1858 -->
|
|
783
1637
|
|
|
784
1638
|
for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
|
|
785
1639
|
DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
|
|
@@ -787,13 +1641,28 @@ for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
|
|
|
787
1641
|
# Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
|
|
788
1642
|
if echo "$DEFER_REASON" | grep -qi "generation"; then
|
|
789
1643
|
PERMANENT_DEFERRED+=($FINDING_NUM)
|
|
1644
|
+
# "Batch fully human-gated" deferrals (forge#1814) are their OWN bucket — they must NOT be
|
|
1645
|
+
# re-evaluated by the file-overlap logic in Step 4F.2 below, because the reason they were
|
|
1646
|
+
# deferred has nothing to do with file overlap. Re-evaluating them the same way as
|
|
1647
|
+
# comment/typo or P3-same-file deferrals would silently undo the idle policy: a sweep can
|
|
1648
|
+
# run while the batch is still a "paused drain" (Step 4B's Termination condition explicitly
|
|
1649
|
+
# allows Step 4F to run in that state), and BATCH_FULLY_GATED would still be true at sweep
|
|
1650
|
+
# time unless a human has actually merged a gating PR in the meantime.
|
|
1651
|
+
elif echo "$DEFER_REASON" | grep -qi "batch fully human-gated"; then
|
|
1652
|
+
IDLE_DEFERRED+=($FINDING_NUM)
|
|
1653
|
+
# "Token budget exceeded" deferrals (forge#1858) are ALSO their own bucket — re-evaluating
|
|
1654
|
+
# them via the file-overlap logic in Step 4F.2 would incorrectly clear them (they were never
|
|
1655
|
+
# deferred for file-overlap reasons) without checking whether token headroom actually exists.
|
|
1656
|
+
# See Step 4F.2.6 below, which mirrors the Step 4F.2.5 idle-gated pattern.
|
|
1657
|
+
elif echo "$DEFER_REASON" | grep -qi "token budget"; then
|
|
1658
|
+
TOKEN_GATED+=($FINDING_NUM)
|
|
790
1659
|
else
|
|
791
1660
|
# All other deferrals (comment/typo, P3 same-file) are re-evaluable
|
|
792
1661
|
SWEEP_CANDIDATES+=($FINDING_NUM)
|
|
793
1662
|
fi
|
|
794
1663
|
done
|
|
795
1664
|
|
|
796
|
-
echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent"
|
|
1665
|
+
echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent, ${#IDLE_DEFERRED[@]} idle-gated, ${#TOKEN_GATED[@]} token-gated"
|
|
797
1666
|
```
|
|
798
1667
|
|
|
799
1668
|
**Step 4F.2: Re-evaluate sweep candidates**
|
|
@@ -829,6 +1698,70 @@ for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
|
|
|
829
1698
|
done
|
|
830
1699
|
```
|
|
831
1700
|
|
|
1701
|
+
**Step 4F.2.5: Re-evaluate idle-gated deferrals** <!-- Added: forge#1814 -->
|
|
1702
|
+
|
|
1703
|
+
Recompute `BATCH_FULLY_GATED` fresh at sweep time (same check as Step 4B item 6.7, over `{all_batch_issue_numbers}`) — do NOT reuse a stale value captured when the finding was originally deferred. If a human has merged a gating PR since the finding was deferred, the original batch is no longer fully gated and the finding is safe to execute; otherwise it stays deferred.
|
|
1704
|
+
|
|
1705
|
+
```bash
|
|
1706
|
+
# Skip the recompute entirely if nothing was idle-gated this run — no need to spend API
|
|
1707
|
+
# calls re-classifying the original batch for a bucket with zero members.
|
|
1708
|
+
if [ "${#IDLE_DEFERRED[@]}" -gt 0 ]; then
|
|
1709
|
+
ANY_ORIGINAL_IN_PROGRESS=false
|
|
1710
|
+
ANY_ORIGINAL_GATED=false
|
|
1711
|
+
for ORIG_NUM in {all_batch_issue_numbers}; do
|
|
1712
|
+
ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
|
|
1713
|
+
case "$ORIG_STATE" in
|
|
1714
|
+
IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
|
|
1715
|
+
GATED) ANY_ORIGINAL_GATED=true ;;
|
|
1716
|
+
DONE|FAILED) ;;
|
|
1717
|
+
esac
|
|
1718
|
+
done
|
|
1719
|
+
if [ "$ANY_ORIGINAL_IN_PROGRESS" = "false" ] && [ "$ANY_ORIGINAL_GATED" = "true" ]; then
|
|
1720
|
+
BATCH_FULLY_GATED=true
|
|
1721
|
+
else
|
|
1722
|
+
BATCH_FULLY_GATED=false
|
|
1723
|
+
fi
|
|
1724
|
+
fi
|
|
1725
|
+
|
|
1726
|
+
for FINDING_NUM in "${IDLE_DEFERRED[@]}"; do
|
|
1727
|
+
if [ "${BATCH_FULLY_GATED:-false}" = "true" ]; then
|
|
1728
|
+
SWEEP_STILL_DEFERRED+=($FINDING_NUM)
|
|
1729
|
+
echo "Sweep: #${FINDING_NUM} still deferred (batch still fully human-gated — idle policy)"
|
|
1730
|
+
else
|
|
1731
|
+
SWEEP_EXECUTE+=($FINDING_NUM)
|
|
1732
|
+
echo "Sweep: #${FINDING_NUM} cleared for execution (batch no longer fully gated — a gating PR merged)"
|
|
1733
|
+
fi
|
|
1734
|
+
done
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
**Step 4F.2.6: Re-evaluate token-gated deferrals** <!-- Added: forge#1858 -->
|
|
1738
|
+
|
|
1739
|
+
The original batch has now drained — the token spend that caused these deferrals belongs to a batch that is no longer competing for dispatch. Re-evaluate `TOKEN_GATED` against a **fresh, sweep-scoped** token allowance (`SWEEP_TOKEN_SPEND`, separate from the drained `BATCH_TOKEN_SPEND` — do NOT reuse or reset the original batch counter, which other reporting still reads in Step 6B). This mirrors the file-overlap re-check in Step 4F.2 (which also benefits from the drained-DAG state) rather than the idle-gated re-check in Step 4F.2.5 (which re-tests an external condition, not a resettable budget).
|
|
1740
|
+
|
|
1741
|
+
```bash
|
|
1742
|
+
# Skip entirely if nothing was token-gated this run.
|
|
1743
|
+
if [ "${#TOKEN_GATED[@]}" -gt 0 ]; then
|
|
1744
|
+
SWEEP_TOKEN_SPEND=0
|
|
1745
|
+
|
|
1746
|
+
for FINDING_NUM in "${TOKEN_GATED[@]}"; do
|
|
1747
|
+
STATE=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json state --jq '.state' 2>/dev/null || echo "")
|
|
1748
|
+
[ "$STATE" = "CLOSED" ] && continue # resolved by another process — drop silently
|
|
1749
|
+
|
|
1750
|
+
PROJECTED_SWEEP_SPEND=$((SWEEP_TOKEN_SPEND + TOKEN_ESTIMATE_PER_FINDING))
|
|
1751
|
+
if [ "$PROJECTED_SWEEP_SPEND" -gt "$TOKEN_BUDGET" ]; then
|
|
1752
|
+
SWEEP_STILL_DEFERRED+=($FINDING_NUM)
|
|
1753
|
+
echo "Sweep: #${FINDING_NUM} still deferred (token budget — sweep allowance ${TOKEN_BUDGET} would be exceeded at ${PROJECTED_SWEEP_SPEND})"
|
|
1754
|
+
else
|
|
1755
|
+
SWEEP_TOKEN_SPEND=$PROJECTED_SWEEP_SPEND
|
|
1756
|
+
SWEEP_EXECUTE+=($FINDING_NUM)
|
|
1757
|
+
echo "Sweep: #${FINDING_NUM} cleared for execution (fits fresh sweep token allowance — ${SWEEP_TOKEN_SPEND}/${TOKEN_BUDGET})"
|
|
1758
|
+
fi
|
|
1759
|
+
done
|
|
1760
|
+
fi
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1763
|
+
Findings that still don't fit the fresh sweep allowance remain deferred — never permanent (unlike `generation >= 2`). They are re-evaluated again on the next `/orchestrate` invocation or completion sweep, exactly like comment/typo and P3-same-file deferrals.
|
|
1764
|
+
|
|
832
1765
|
**Step 4F.3: Dispatch cleared findings**
|
|
833
1766
|
|
|
834
1767
|
For each finding in `SWEEP_EXECUTE`, add it to a fresh sweep DAG and dispatch using the same Steps 4A.pre.0, 4A.pre, and 4A logic. Run file-overlap detection between the swept findings themselves (they may conflict with each other).
|
|
@@ -888,7 +1821,10 @@ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
|
|
|
888
1821
|
# synthesis brief (Phase 2.5 runs only over the original batch's investigations), so
|
|
889
1822
|
# there is nothing to prefer. Keep this block in sync with 4A's fallback branch only.
|
|
890
1823
|
GIST_CONTEXT=""
|
|
1824
|
+
# Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching —
|
|
1825
|
+
# kept in sync with 4A's fallback branch above.
|
|
891
1826
|
PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
|
|
1827
|
+
| sed -E 's/[*_]+//g' \
|
|
892
1828
|
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
893
1829
|
|
|
894
1830
|
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
@@ -910,7 +1846,7 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
910
1846
|
# the complete pipeline contract. Keep in sync with Step 4A when the template changes.
|
|
911
1847
|
Agent(
|
|
912
1848
|
subagent_type="general-purpose",
|
|
913
|
-
model="
|
|
1849
|
+
model="{SUBAGENT_MODEL}",
|
|
914
1850
|
description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
|
|
915
1851
|
run_in_background=true,
|
|
916
1852
|
prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
|
|
@@ -921,21 +1857,21 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
921
1857
|
|
|
922
1858
|
**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.
|
|
923
1859
|
|
|
924
|
-
**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\`, or \`
|
|
1860
|
+
**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.
|
|
925
1861
|
|
|
926
1862
|
**HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with \`review-finding\` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
|
|
927
1863
|
|
|
928
1864
|
**IMPORTANT RULES**:
|
|
929
1865
|
- **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '${FINDING_NUM}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
|
|
930
|
-
- For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
|
|
931
|
-
- For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM}')\` (prefix from forge.yaml → repos.satellites)
|
|
1866
|
+
- For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM} --under-orchestration')\`
|
|
1867
|
+
- For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM} --under-orchestration')\` (prefix from forge.yaml → repos.satellites)
|
|
932
1868
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
933
1869
|
- NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
|
|
934
1870
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
|
|
935
1871
|
- If the issue is INVALID after investigation, close it with a comment explaining why
|
|
936
1872
|
- If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
|
|
937
1873
|
- Do not interact with the user — you are running autonomously in the background
|
|
938
|
-
- **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed
|
|
1874
|
+
- **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed, then let `/review-pr`'s own verdict decide: APPROVED (no unresolved CONFIRMED HIGH/CRITICAL finding) → merge to `staging` and create follow-up issues for the rest, **regardless of domain**. Domain alone (AUTH, BILLING, DATABASE, or any domain tagged as security-critical in Step 3B) is NOT a reason to add `needs-human` and stop — `staging` is reversible; the real human deploy gate is `staging → main`, not this merge. <!-- Added: forge#1815 --> `needs-human` is reserved for what the pipeline genuinely cannot do itself: spend/procurement decisions, real-environment validation it has no access to, product/architecture judgment calls a human must make, or `/review-pr`'s existing evidence-based escalations (spec-evolution guard, novel task-type/module-combo trust escalation, calibration-based overconfidence routing). An unresolved CONFIRMED HIGH/CRITICAL finding is `/review-pr`'s own withheld-APPROVED case, not a domain-driven `needs-human` halt — `/review-pr` already refuses to return APPROVED when that's true, so there is no separate domain check to perform here.
|
|
939
1875
|
|
|
940
1876
|
**LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
|
|
941
1877
|
After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
|
|
@@ -943,8 +1879,8 @@ After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the
|
|
|
943
1879
|
gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
|
|
944
1880
|
\`\`\`
|
|
945
1881
|
**Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
|
|
946
|
-
**Terminal condition also**: \`needs-human\` label present OR issue state is \`closed\`
|
|
947
|
-
If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM}')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
|
|
1882
|
+
**Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
|
|
1883
|
+
If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM} --under-orchestration')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
|
|
948
1884
|
|
|
949
1885
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
950
1886
|
- If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
|
|
@@ -973,14 +1909,52 @@ Completion Sweep Results:
|
|
|
973
1909
|
Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
|
|
974
1910
|
Still deferred (cosmetic): #{C} (comment/typo)
|
|
975
1911
|
Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
|
|
1912
|
+
Idle-gated — still deferred: #{E} (batch still fully human-gated — waiting on a merge)
|
|
1913
|
+
Idle-gated — cleared: #{F} (a gating PR merged since deferral — no longer idle)
|
|
1914
|
+
Token-gated — cleared: #{G} (fits fresh sweep token allowance — batch spend ${SWEEP_TOKEN_SPEND}/${TOKEN_BUDGET})
|
|
1915
|
+
Token-gated — still deferred: #{H} (sweep allowance also exhausted — re-evaluable next run)
|
|
976
1916
|
```
|
|
977
1917
|
|
|
978
|
-
**After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
|
|
1918
|
+
**After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
|
|
979
1919
|
|
|
980
1920
|
**Anti-patterns — DO NOT DO THIS:**
|
|
981
1921
|
- Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
|
|
982
1922
|
- Overriding generation >= 2 deferrals — the cascade cap is absolute.
|
|
983
1923
|
- Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
|
|
1924
|
+
- Clearing an idle-gated deferral (forge#1814) without recomputing `BATCH_FULLY_GATED` fresh at sweep time — a stale "not gated" read would re-introduce the exact net-negative churn this policy exists to stop.
|
|
1925
|
+
- Clearing a token-gated deferral (forge#1858) by reusing the drained `BATCH_TOKEN_SPEND` instead of the fresh `SWEEP_TOKEN_SPEND` allowance — the whole point of Step 4F.2.6 is that the *original* batch's spend is no longer relevant once it has drained.
|
|
1926
|
+
|
|
1927
|
+
### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
|
|
1928
|
+
|
|
1929
|
+
**Run only when `BUDGET_LIMIT != "Infinity"` AND `${#DEFERRED_BUDGET_ISSUES[@]} > 0`.**
|
|
1930
|
+
|
|
1931
|
+
Deferred issues are issues that were not dispatched because their estimated cost would have pushed projected spend past the budget ceiling (after reserving ε for no-prior issues). They are **never silently dropped** — this report makes them visible and actionable.
|
|
1932
|
+
|
|
1933
|
+
```bash
|
|
1934
|
+
if [ "$BUDGET_LIMIT" != "Infinity" ] && [ "${#DEFERRED_BUDGET_ISSUES[@]}" -gt 0 ]; then
|
|
1935
|
+
echo ""
|
|
1936
|
+
echo "## Budget Report"
|
|
1937
|
+
echo ""
|
|
1938
|
+
echo "**Budget limit**: \$${BUDGET_LIMIT}"
|
|
1939
|
+
echo "**Projected spend (dispatched issues)**: \$${PROJECTED_SPEND}"
|
|
1940
|
+
echo "**Actual spend (completed issues, best-effort)**: \$${ACTUAL_SPEND}"
|
|
1941
|
+
echo "**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = \$${EPSILON_BUDGET})"
|
|
1942
|
+
echo ""
|
|
1943
|
+
echo "### Deferred Issues (budget exhausted — never silently dropped)"
|
|
1944
|
+
echo ""
|
|
1945
|
+
echo "| Issue | Title | Score | Est. Cost | Reason |"
|
|
1946
|
+
echo "|-------|-------|-------|-----------|--------|"
|
|
1947
|
+
for DNUM in "${DEFERRED_BUDGET_ISSUES[@]}"; do
|
|
1948
|
+
DTITLE=$(gh issue view "$DNUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "(unknown)")
|
|
1949
|
+
DSCORE="${ISSUE_SCORE[$DNUM]:-?}"
|
|
1950
|
+
DCOST="${ISSUE_COST_ESTIMATE[$DNUM]:-?}"
|
|
1951
|
+
echo "| #${DNUM} | ${DTITLE} | ${DSCORE} | \$${DCOST} | Budget ceiling reached |"
|
|
1952
|
+
done
|
|
1953
|
+
echo ""
|
|
1954
|
+
DEFERRED_LIST=$(IFS=' '; echo "${DEFERRED_BUDGET_ISSUES[*]}")
|
|
1955
|
+
echo "**Action**: Re-run \`/orchestrate ${DEFERRED_LIST} [--budget N]\` to process deferred issues, or increase \`--budget\`."
|
|
1956
|
+
fi
|
|
1957
|
+
```
|
|
984
1958
|
|
|
985
1959
|
---
|
|
986
1960
|
|