forgedock 1.1.8 → 1.1.10
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-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/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +935 -19
- package/bin/recall.mjs +24 -7
- 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 +1 -1
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +87 -52
- 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 +1 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- 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 +128 -20
- package/commands/orchestrate/phase-4-execution.md +598 -43
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +51 -3
- package/commands/orchestrate/safety.md +2 -2
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +1 -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 +71 -6
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +1 -1
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/context.md +9 -6
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +1 -1
- package/commands/work-on/build.md +31 -6
- package/commands/work-on/close.md +3 -2
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +14 -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 +194 -56
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -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/label-state-machine.md +1 -1
- package/package.json +2 -1
- package/packages/protocol/src/types.js +1 -1
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +19 -12
- package/scripts/transition-label.sh +101 -13
- 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,8 @@ 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
|
+
|
|
11
13
|
### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
|
|
12
14
|
|
|
13
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).
|
|
@@ -95,6 +97,47 @@ should_dispatch() {
|
|
|
95
97
|
|
|
96
98
|
**When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
|
|
97
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
|
+
|
|
98
141
|
### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
|
|
99
142
|
|
|
100
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.
|
|
@@ -222,7 +265,7 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
|
|
|
222
265
|
|
|
223
266
|
```bash
|
|
224
267
|
# Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
|
|
225
|
-
# (installed by `npx forgedock` — see bin/
|
|
268
|
+
# (installed by `npx forgedock` — see bin/journey.mjs: forge()'s linkPipelineScripts() step)
|
|
226
269
|
# Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
|
|
227
270
|
|
|
228
271
|
declare -A ISSUE_LANE
|
|
@@ -236,6 +279,43 @@ QUEUED_FINDINGS=()
|
|
|
236
279
|
declare -A DEFERRED_REASONS
|
|
237
280
|
declare -A AGENT_ISSUE_MAP
|
|
238
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
|
+
|
|
239
319
|
for NUM in {ready_issue_numbers}; do
|
|
240
320
|
PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
|
|
241
321
|
echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
|
|
@@ -266,24 +346,38 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
|
|
|
266
346
|
FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
|
|
267
347
|
|
|
268
348
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
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=()
|
|
269
352
|
for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
fi
|
|
275
|
-
|
|
276
|
-
LANE="${ISSUE_LANE[$NUM]}"
|
|
277
|
-
PR_BASE="${ISSUE_PR_BASE[$NUM]}"
|
|
278
|
-
COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
|
|
279
|
-
|
|
280
|
-
# Advance PROJECTED_SPEND before forking so subsequent iterations see the updated total
|
|
281
|
-
PROJECTED_SPEND=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
|
|
353
|
+
if should_dispatch "$NUM"; then
|
|
354
|
+
DISPATCH_QUEUE+=("$NUM")
|
|
355
|
+
fi # else: added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
|
|
356
|
+
done
|
|
282
357
|
|
|
283
|
-
|
|
284
|
-
|
|
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))
|
|
285
380
|
done
|
|
286
|
-
wait
|
|
287
381
|
echo "Engine dispatch complete — advancing to Step 4B (completion sweep)"
|
|
288
382
|
else
|
|
289
383
|
echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
|
|
@@ -305,7 +399,7 @@ For each **ready** issue (all predecessors resolved or no predecessors), spawn a
|
|
|
305
399
|
```
|
|
306
400
|
Agent(
|
|
307
401
|
subagent_type="general-purpose",
|
|
308
|
-
model="
|
|
402
|
+
model="{SUBAGENT_MODEL}",
|
|
309
403
|
description="Work on {PROJECT_PREFIX}#{NUMBER}",
|
|
310
404
|
run_in_background=true,
|
|
311
405
|
prompt="You are working on GitHub issue #{NUMBER} for the {PROJECT_NAME} project.
|
|
@@ -322,15 +416,16 @@ Agent(
|
|
|
322
416
|
|
|
323
417
|
**IMPORTANT RULES**:
|
|
324
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.
|
|
325
|
-
- For default repo issues: `Skill(skill='work-on', args='{NUMBER}')`
|
|
326
|
-
- 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).
|
|
327
422
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
328
423
|
- NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
|
|
329
424
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
|
|
330
425
|
- If the issue is INVALID after investigation, close it with a comment explaining why
|
|
331
426
|
- If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
|
|
332
427
|
- Do not interact with the user — you are running autonomously in the background
|
|
333
|
-
- **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.
|
|
334
429
|
|
|
335
430
|
**LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
|
|
336
431
|
After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the issue's current workflow label:
|
|
@@ -340,7 +435,7 @@ gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select
|
|
|
340
435
|
**Terminal labels** (only these allow you to stop): `workflow:merged`, `workflow:invalid`
|
|
341
436
|
**Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
|
|
342
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`).
|
|
343
|
-
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}')` 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.
|
|
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.
|
|
344
439
|
|
|
345
440
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
346
441
|
- If the issue has the `review-finding` label, read the issue body for `**Code branch**: \`{branch}\``
|
|
@@ -374,7 +469,11 @@ if [ -n "$SYNTHESIS_BRIEF" ]; then
|
|
|
374
469
|
${SYNTHESIS_BRIEF}"
|
|
375
470
|
else
|
|
376
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.
|
|
377
475
|
PARENT_INV=$(gh issue view {NUMBER} -R {GH_REPO} --json body --jq '.body' \
|
|
476
|
+
| sed -E 's/[*_]+//g' \
|
|
378
477
|
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
379
478
|
|
|
380
479
|
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
@@ -446,6 +545,20 @@ fi
|
|
|
446
545
|
|
|
447
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.
|
|
448
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
|
+
|
|
449
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:
|
|
450
563
|
|
|
451
564
|
```
|
|
@@ -455,12 +568,43 @@ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
|
|
|
455
568
|
|
|
456
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 -->
|
|
457
570
|
|
|
458
|
-
**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).
|
|
459
599
|
|
|
460
600
|
### Step 4B: Monitor completions and dispatch newly ready issues
|
|
461
601
|
|
|
462
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.
|
|
463
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
|
+
|
|
464
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 -->
|
|
465
609
|
|
|
466
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:
|
|
@@ -501,6 +645,94 @@ classify_predecessor_state() {
|
|
|
501
645
|
|
|
502
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.
|
|
503
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
|
+
|
|
504
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.
|
|
505
737
|
|
|
506
738
|
```bash
|
|
@@ -512,7 +744,50 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
|
512
744
|
for PRED in {predecessors_of_BLOCKED_NUM}; do
|
|
513
745
|
PRED_STATE=$(classify_predecessor_state "$PRED")
|
|
514
746
|
case "$PRED_STATE" in
|
|
515
|
-
DONE)
|
|
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
|
|
516
791
|
FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
|
|
517
792
|
GATED)
|
|
518
793
|
ALL_PREDS_DONE=false
|
|
@@ -530,7 +805,11 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
|
530
805
|
# Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
|
|
531
806
|
fi
|
|
532
807
|
done
|
|
533
|
-
# 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.
|
|
534
813
|
```
|
|
535
814
|
|
|
536
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:
|
|
@@ -552,10 +831,10 @@ done
|
|
|
552
831
|
resume=AGENT_ISSUE_MAP[{NUMBER}],
|
|
553
832
|
description="Resume #{NUMBER} pipeline",
|
|
554
833
|
run_in_background=true,
|
|
555
|
-
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."
|
|
556
835
|
)
|
|
557
836
|
```
|
|
558
|
-
**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 -->
|
|
559
838
|
|
|
560
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.
|
|
561
840
|
|
|
@@ -601,11 +880,78 @@ done
|
|
|
601
880
|
|
|
602
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
|
|
603
882
|
|
|
604
|
-
5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready,
|
|
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
|
+
```
|
|
605
933
|
|
|
606
|
-
|
|
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.
|
|
607
935
|
|
|
608
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)**:
|
|
609
955
|
```bash
|
|
610
956
|
# Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
|
|
611
957
|
# do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
|
|
@@ -654,6 +1000,56 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
654
1000
|
```
|
|
655
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.
|
|
656
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.
|
|
1042
|
+
|
|
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}
|
|
1047
|
+
|
|
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.
|
|
1052
|
+
|
|
657
1053
|
7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
|
|
658
1054
|
```bash
|
|
659
1055
|
LABELS=$(gh issue view $NUM -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))] | length')
|
|
@@ -664,7 +1060,9 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
664
1060
|
```
|
|
665
1061
|
If an agent bypassed the pipeline, report it as a **failure** regardless of whether a PR exists.
|
|
666
1062
|
|
|
667
|
-
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:
|
|
668
1066
|
```
|
|
669
1067
|
✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
|
|
670
1068
|
✗ #{NUMBER} — {title} → {reason for failure}
|
|
@@ -675,6 +1073,17 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
675
1073
|
→ Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
|
|
676
1074
|
```
|
|
677
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
|
+
|
|
678
1087
|
`⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
|
|
679
1088
|
two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
|
|
680
1089
|
`⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
|
|
@@ -686,6 +1095,10 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
686
1095
|
|
|
687
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.
|
|
688
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.
|
|
1101
|
+
|
|
689
1102
|
**Anti-pattern — DO NOT DO THIS:**
|
|
690
1103
|
- `sleep 60/120/180/300` loops to check status — you will be notified automatically
|
|
691
1104
|
- Spawning separate "progress check" agents — they waste tokens and add noise
|
|
@@ -805,16 +1218,18 @@ gh issue list -R {GH_REPO} --state open --label "review-finding" --limit 20 \
|
|
|
805
1218
|
For each spawned finding, determine whether it should be **executed** or **deferred**:
|
|
806
1219
|
|
|
807
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.
|
|
808
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.
|
|
809
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.
|
|
810
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.
|
|
811
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).
|
|
812
1227
|
|
|
813
|
-
**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.
|
|
814
1229
|
|
|
815
1230
|
**Execute** (add to the DAG) if:
|
|
816
|
-
- Rule 2 matches (P1 or P2)
|
|
817
|
-
- 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
|
|
818
1233
|
|
|
819
1234
|
**Before running the loop, build the batch file list (MANDATORY for Heuristic 3):**
|
|
820
1235
|
|
|
@@ -850,8 +1265,13 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
850
1265
|
PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
|
|
851
1266
|
TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
|
|
852
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"
|
|
853
1273
|
# Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
|
|
854
|
-
|
|
1274
|
+
elif SOURCE_NUM=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oP '(?i)spawned from issue #\K\d+|source issue[: #]+\K\d+' | head -1) && \
|
|
855
1275
|
[ -n "$SOURCE_NUM" ] && \
|
|
856
1276
|
gh issue view $SOURCE_NUM -R {GH_REPO} --json labels --jq '[.labels[].name]' 2>/dev/null | grep -q "review-finding"; then
|
|
857
1277
|
DEFER=true; DEFER_REASON="generation >= 2 (source #${SOURCE_NUM} is also a review-finding)"
|
|
@@ -893,8 +1313,14 @@ Cascade-spawned findings collected within a single `/orchestrate` run never pass
|
|
|
893
1313
|
# exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
|
|
894
1314
|
# with identical patterns — NOT grep ERE — so the two batching checks cannot
|
|
895
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
|
|
896
1323
|
declare -A SURFACE_FILE_MEMBERS
|
|
897
|
-
SURFACE_BATCHED_FINDINGS=()
|
|
898
1324
|
|
|
899
1325
|
# Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
|
|
900
1326
|
# upstream cascade control; this cap holds even if that bound is later loosened,
|
|
@@ -983,6 +1409,7 @@ BATCH_EOF
|
|
|
983
1409
|
# with the single batch issue, so the dispatch step below operates on the
|
|
984
1410
|
# batch unit and skips the individual members.
|
|
985
1411
|
SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
|
|
1412
|
+
[ -n "$BATCH_ISSUE_NUM" ] && SURFACE_BATCH_COUNT=$((SURFACE_BATCH_COUNT + 1)) # forge#1858 Step 6B counter
|
|
986
1413
|
QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
|
|
987
1414
|
[ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
|
|
988
1415
|
echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
|
|
@@ -992,6 +1419,44 @@ done
|
|
|
992
1419
|
|
|
993
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.
|
|
994
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
|
+
|
|
995
1460
|
**For queued (non-deferred) findings:**
|
|
996
1461
|
|
|
997
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.
|
|
@@ -1000,7 +1465,7 @@ Findings clustered here are replaced by their batch issue in `QUEUED_FINDINGS` (
|
|
|
1000
1465
|
```
|
|
1001
1466
|
Agent #{COMPLETED} spawned {count} new finding issues: #{A}, #{B}
|
|
1002
1467
|
Added to DAG: #{A} (predecessors: {}), #{B} (predecessors: {#{X}})
|
|
1003
|
-
Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic)
|
|
1468
|
+
Deferred (cascade control): #{C} (P3 same-file), #{D} (comment heuristic), #{E} (token budget)
|
|
1004
1469
|
```
|
|
1005
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.
|
|
1006
1471
|
|
|
@@ -1162,11 +1627,13 @@ If an agent reports failure or error:
|
|
|
1162
1627
|
|
|
1163
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.
|
|
1164
1629
|
|
|
1165
|
-
**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**
|
|
1166
1631
|
|
|
1167
1632
|
```bash
|
|
1168
1633
|
PERMANENT_DEFERRED=()
|
|
1169
1634
|
SWEEP_CANDIDATES=()
|
|
1635
|
+
IDLE_DEFERRED=() # <!-- Added: forge#1814 -->
|
|
1636
|
+
TOKEN_GATED=() # <!-- Added: forge#1858 -->
|
|
1170
1637
|
|
|
1171
1638
|
for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
|
|
1172
1639
|
DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
|
|
@@ -1174,13 +1641,28 @@ for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
|
|
|
1174
1641
|
# Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
|
|
1175
1642
|
if echo "$DEFER_REASON" | grep -qi "generation"; then
|
|
1176
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)
|
|
1177
1659
|
else
|
|
1178
1660
|
# All other deferrals (comment/typo, P3 same-file) are re-evaluable
|
|
1179
1661
|
SWEEP_CANDIDATES+=($FINDING_NUM)
|
|
1180
1662
|
fi
|
|
1181
1663
|
done
|
|
1182
1664
|
|
|
1183
|
-
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"
|
|
1184
1666
|
```
|
|
1185
1667
|
|
|
1186
1668
|
**Step 4F.2: Re-evaluate sweep candidates**
|
|
@@ -1216,6 +1698,70 @@ for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
|
|
|
1216
1698
|
done
|
|
1217
1699
|
```
|
|
1218
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
|
+
|
|
1219
1765
|
**Step 4F.3: Dispatch cleared findings**
|
|
1220
1766
|
|
|
1221
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).
|
|
@@ -1275,7 +1821,10 @@ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
|
|
|
1275
1821
|
# synthesis brief (Phase 2.5 runs only over the original batch's investigations), so
|
|
1276
1822
|
# there is nothing to prefer. Keep this block in sync with 4A's fallback branch only.
|
|
1277
1823
|
GIST_CONTEXT=""
|
|
1824
|
+
# Markdown emphasis markers (**bold**, __bold__, *italic*) are stripped before matching —
|
|
1825
|
+
# kept in sync with 4A's fallback branch above.
|
|
1278
1826
|
PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
|
|
1827
|
+
| sed -E 's/[*_]+//g' \
|
|
1279
1828
|
| grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
|
|
1280
1829
|
|
|
1281
1830
|
if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
|
|
@@ -1297,7 +1846,7 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
1297
1846
|
# the complete pipeline contract. Keep in sync with Step 4A when the template changes.
|
|
1298
1847
|
Agent(
|
|
1299
1848
|
subagent_type="general-purpose",
|
|
1300
|
-
model="
|
|
1849
|
+
model="{SUBAGENT_MODEL}",
|
|
1301
1850
|
description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
|
|
1302
1851
|
run_in_background=true,
|
|
1303
1852
|
prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
|
|
@@ -1314,15 +1863,15 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
1314
1863
|
|
|
1315
1864
|
**IMPORTANT RULES**:
|
|
1316
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.
|
|
1317
|
-
- For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
|
|
1318
|
-
- 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)
|
|
1319
1868
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
1320
1869
|
- NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
|
|
1321
1870
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
|
|
1322
1871
|
- If the issue is INVALID after investigation, close it with a comment explaining why
|
|
1323
1872
|
- If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
|
|
1324
1873
|
- Do not interact with the user — you are running autonomously in the background
|
|
1325
|
-
- **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.
|
|
1326
1875
|
|
|
1327
1876
|
**LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
|
|
1328
1877
|
After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
|
|
@@ -1331,7 +1880,7 @@ gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name |
|
|
|
1331
1880
|
\`\`\`
|
|
1332
1881
|
**Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
|
|
1333
1882
|
**Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
|
|
1334
|
-
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.
|
|
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.
|
|
1335
1884
|
|
|
1336
1885
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
1337
1886
|
- If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
|
|
@@ -1360,6 +1909,10 @@ Completion Sweep Results:
|
|
|
1360
1909
|
Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
|
|
1361
1910
|
Still deferred (cosmetic): #{C} (comment/typo)
|
|
1362
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)
|
|
1363
1916
|
```
|
|
1364
1917
|
|
|
1365
1918
|
**After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
|
|
@@ -1368,6 +1921,8 @@ Completion Sweep Results:
|
|
|
1368
1921
|
- Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
|
|
1369
1922
|
- Overriding generation >= 2 deferrals — the cascade cap is absolute.
|
|
1370
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.
|
|
1371
1926
|
|
|
1372
1927
|
### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
|
|
1373
1928
|
|