forgedock 1.1.6 → 1.1.8
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 +3 -1
- package/bin/engine/invariants.mjs +361 -0
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/journey.mjs +77 -7
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +540 -0
- package/commands/autopilot.md +959 -78
- package/commands/cleanup.md +52 -3
- package/commands/optimize.md +585 -0
- package/commands/orchestrate/phase-1-resolve.md +35 -25
- package/commands/orchestrate/phase-3-dependency.md +465 -34
- package/commands/orchestrate/phase-4-execution.md +448 -29
- package/commands/orchestrate/phase-6-report.md +198 -9
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/quality-gate.md +417 -0
- package/commands/review-pr.md +391 -13
- package/commands/spec-doctor.md +429 -0
- package/commands/validate.md +52 -0
- package/commands/work-on/build/architect.md +77 -0
- package/commands/work-on/build/context.md +357 -22
- package/commands/work-on/build/implement.md +2 -0
- package/commands/work-on/build/validate.md +28 -0
- package/commands/work-on/build.md +44 -0
- package/commands/work-on/close.md +621 -8
- package/commands/work-on/investigate.md +221 -0
- package/commands/work-on.md +15 -14
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/design/awesome-list-submissions.md +105 -0
- package/docs/design/f5bot-monitoring-setup.md +43 -0
- package/docs/design/growth-strategy-2026.md +579 -0
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +2 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/danger-zones.mjs +782 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/refresh-bot-token.sh +97 -0
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +157 -6
|
@@ -8,6 +8,93 @@ install: internal
|
|
|
8
8
|
|
|
9
9
|
## Phase 4: Streaming DAG Execution
|
|
10
10
|
|
|
11
|
+
### Step 4A-pre.0: Budget initialization (MANDATORY when --budget is set) <!-- Added: forge#1743 -->
|
|
12
|
+
|
|
13
|
+
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).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# --- Budget initialization ---
|
|
17
|
+
# Parse --budget N from ARGUMENTS (e.g. /orchestrate fast-lane --budget 5.00)
|
|
18
|
+
BUDGET_LIMIT=$(echo "${ARGUMENTS:-}" | grep -oP '(?<=--budget )\S+' | head -1 || echo "")
|
|
19
|
+
if [ -z "$BUDGET_LIMIT" ] || ! echo "$BUDGET_LIMIT" | grep -qP '^\d+(\.\d+)?$'; then
|
|
20
|
+
BUDGET_LIMIT="Infinity"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
PROJECTED_SPEND="0" # sum of ISSUE_COST_ESTIMATE[] for dispatched issues
|
|
24
|
+
ACTUAL_SPEND="0" # sum of actual cost reported by completed agents (best-effort)
|
|
25
|
+
DEFERRED_BUDGET_ISSUES=() # issues deferred because projected spend would exceed budget
|
|
26
|
+
EPSILON_DISPATCHED=false # true once at least one ε-reserve issue has been dispatched
|
|
27
|
+
|
|
28
|
+
if [ "$BUDGET_LIMIT" != "Infinity" ]; then
|
|
29
|
+
EPSILON_BUDGET=$(echo "scale=4; $BUDGET_LIMIT * 0.10" | bc 2>/dev/null || echo "0")
|
|
30
|
+
echo "Budget initialized: BUDGET_LIMIT=\$${BUDGET_LIMIT} EPSILON_BUDGET=\$${EPSILON_BUDGET} (10% ε-reserve)"
|
|
31
|
+
echo "Issues without cost priors (ε-reserve eligible): ${NO_PRIOR_ISSUES[*]:-none}"
|
|
32
|
+
else
|
|
33
|
+
EPSILON_BUDGET="0"
|
|
34
|
+
echo "Budget: uncapped (no --budget flag) — dispatching all ready issues by score order"
|
|
35
|
+
fi
|
|
36
|
+
# --- End budget initialization ---
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Budget halt condition** (checked in Step 4A before each dispatch):
|
|
40
|
+
|
|
41
|
+
Before dispatching issue `NUM`, check whether its estimated cost would exceed the remaining budget:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Check budget before dispatching NUM
|
|
45
|
+
should_dispatch() {
|
|
46
|
+
local NUM="$1"
|
|
47
|
+
local COST="${ISSUE_COST_ESTIMATE[$NUM]:-0.35}"
|
|
48
|
+
|
|
49
|
+
if [ "$BUDGET_LIMIT" = "Infinity" ]; then
|
|
50
|
+
return 0 # uncapped — always dispatch
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
NEW_PROJECTED=$(echo "scale=4; $PROJECTED_SPEND + $COST" | bc 2>/dev/null || echo "$PROJECTED_SPEND")
|
|
54
|
+
MAIN_CEILING=$(echo "scale=4; $BUDGET_LIMIT - $EPSILON_BUDGET" | bc 2>/dev/null || echo "$BUDGET_LIMIT")
|
|
55
|
+
|
|
56
|
+
# ε-reserve logic: if this is a no-prior issue AND ε-budget has not yet been
|
|
57
|
+
# used AND the no-prior issue has NOT been dispatched yet, allow it even if
|
|
58
|
+
# the main ceiling is hit (up to BUDGET_LIMIT total).
|
|
59
|
+
if [ "${ISSUE_HAS_PRIOR[$NUM]:-false}" = "false" ] && [ "$EPSILON_DISPATCHED" = "false" ]; then
|
|
60
|
+
if echo "$NEW_PROJECTED $BUDGET_LIMIT" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
|
|
61
|
+
EPSILON_DISPATCHED=true
|
|
62
|
+
echo "ε-reserve: dispatching #${NUM} (no-prior issue) from exploration reserve"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Main budget check: defer if projected would exceed main ceiling
|
|
68
|
+
if echo "$NEW_PROJECTED $MAIN_CEILING" | awk '{exit ($1 <= $2) ? 0 : 1}' 2>/dev/null; then
|
|
69
|
+
DEFERRED_BUDGET_ISSUES+=("$NUM")
|
|
70
|
+
echo "BUDGET DEFER: #${NUM} (est. \$${COST}) would push projected spend to \$${NEW_PROJECTED} > main ceiling \$${MAIN_CEILING}"
|
|
71
|
+
return 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Budget deferred-issues report** (output when `BUDGET_LIMIT` is finite and `DEFERRED_BUDGET_ISSUES` is non-empty — print at end of Phase 4, before Phase 5):
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
## Budget Report
|
|
82
|
+
|
|
83
|
+
**Budget limit**: $${BUDGET_LIMIT}
|
|
84
|
+
**Projected spend (dispatched issues)**: $${PROJECTED_SPEND}
|
|
85
|
+
**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = $${EPSILON_BUDGET})
|
|
86
|
+
|
|
87
|
+
### Deferred Issues (budget exhausted — never silently dropped)
|
|
88
|
+
|
|
89
|
+
| Issue | Title | Score | Est. Cost | Reason |
|
|
90
|
+
|-------|-------|-------|-----------|--------|
|
|
91
|
+
| #{N} | {title} | {score} | ${cost} | Budget ceiling reached |
|
|
92
|
+
|
|
93
|
+
**Action**: Re-run `/orchestrate {deferred_issue_numbers} [--budget N]` to process deferred issues, or increase `--budget`.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**When `BUDGET_LIMIT = Infinity`**: skip this check and report entirely — uncapped behavior.
|
|
97
|
+
|
|
11
98
|
### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
|
|
12
99
|
|
|
13
100
|
**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.
|
|
@@ -174,20 +261,32 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
|
|
|
174
261
|
**Engine-first dispatch (default)**: When `forgedock` is in PATH, dispatch each ready issue via the durable engine rather than spawning prose Agent sub-agents. The engine's phase table enforces gate semantics in code — its fail-closed review gate and deterministic phase ordering are not subject to LLM interpretation.
|
|
175
262
|
|
|
176
263
|
```bash
|
|
177
|
-
# Engine-first dispatch: check CLI availability, then dispatch ready issues in
|
|
264
|
+
# Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
|
|
265
|
+
# Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
|
|
178
266
|
FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
|
|
179
267
|
|
|
180
268
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
181
|
-
for NUM in {ready_issue_numbers}; do
|
|
269
|
+
for NUM in "${SORTED_READY_SET[@]:-{ready_issue_numbers}}"; do
|
|
270
|
+
# Budget gate (forge#1743): skip dispatch if projected spend would exceed budget ceiling.
|
|
271
|
+
# should_dispatch() also handles ε-reserve (no-prior issues get guaranteed slot).
|
|
272
|
+
if ! should_dispatch "$NUM"; then
|
|
273
|
+
continue # Issue added to DEFERRED_BUDGET_ISSUES[] by should_dispatch()
|
|
274
|
+
fi
|
|
275
|
+
|
|
182
276
|
LANE="${ISSUE_LANE[$NUM]}"
|
|
183
277
|
PR_BASE="${ISSUE_PR_BASE[$NUM]}"
|
|
184
|
-
|
|
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")
|
|
282
|
+
|
|
283
|
+
echo "Dispatching #$NUM via forgedock run-issue --lane $PR_BASE (score=${ISSUE_SCORE[$NUM]:-?} est_cost=\$${COST} projected_total=\$${PROJECTED_SPEND})"
|
|
185
284
|
forgedock run-issue "$NUM" --lane "$PR_BASE" &
|
|
186
285
|
done
|
|
187
286
|
wait
|
|
188
287
|
echo "Engine dispatch complete — advancing to Step 4B (completion sweep)"
|
|
189
288
|
else
|
|
190
|
-
echo "
|
|
289
|
+
echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
|
|
191
290
|
# Fall through to Agent-spawn template below. The SubagentStop hook (bin/hooks/interactive-engine.mjs)
|
|
192
291
|
# bridges these runs to the engine run-log for state persistence even on the fallback path.
|
|
193
292
|
fi
|
|
@@ -217,7 +316,7 @@ Agent(
|
|
|
217
316
|
|
|
218
317
|
**YOUR MISSION**: Invoke `/work-on` via the Skill tool and let it run to completion. `/work-on` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — `/work-on` handles everything including issue closure and label updates in its close phase.
|
|
219
318
|
|
|
220
|
-
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: `workflow:merged`, `workflow:invalid`, or `
|
|
319
|
+
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: `workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
|
|
221
320
|
|
|
222
321
|
**HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with `review-finding` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
|
|
223
322
|
|
|
@@ -239,7 +338,8 @@ After EVERY `Skill(skill='work-on', ...)` call returns, immediately check the is
|
|
|
239
338
|
gh issue view {NUMBER} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith("workflow:"))]'
|
|
240
339
|
```
|
|
241
340
|
**Terminal labels** (only these allow you to stop): `workflow:merged`, `workflow:invalid`
|
|
242
|
-
**Terminal condition also**: `needs-human` label present OR issue state is `closed`
|
|
341
|
+
**Terminal condition also**: `needs-human` label present, `workflow:awaiting-merge` label present, OR issue state is `closed`
|
|
342
|
+
`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`).
|
|
243
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.
|
|
244
344
|
|
|
245
345
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
@@ -297,6 +397,30 @@ fi
|
|
|
297
397
|
|
|
298
398
|
If `GIST_CONTEXT` is empty (no synthesis brief, no parent investigation, and no milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341, forge#1192 -->
|
|
299
399
|
|
|
400
|
+
**Claims board context injection** <!-- Added: forge#1736 -->: When a coordination issue exists for this batch (`FORGE_COORD_ISSUE` is set), append the claims board URL and the active-claims check instruction to the agent's context. This enables each `/work-on` agent to post its `FORGE:CLAIM` on build start.
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# Inject coordination issue URL if claims board was created in Step 3D.1
|
|
404
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
|
|
405
|
+
GIST_CONTEXT="${GIST_CONTEXT}
|
|
406
|
+
|
|
407
|
+
**ORCHESTRATION CLAIMS BOARD**: This agent is running under an orchestration batch.
|
|
408
|
+
Claims board issue URL: ${FORGE_COORD_ISSUE}
|
|
409
|
+
|
|
410
|
+
On build start (Phase B2 / Phase 3C of /work-on), post a FORGE:CLAIM annotation on the
|
|
411
|
+
coordination issue above. Required fields:
|
|
412
|
+
Holder: ##{NUMBER} / batch-$(date -u +%Y%m%dT%H%M%S)
|
|
413
|
+
Files: (list of files from your FORGE:CONTRACT deliverables table, one per line)
|
|
414
|
+
Interfaces: (public function/type signatures you will modify or that callers must preserve)
|
|
415
|
+
TTL: terminal state of Holder issue ##{NUMBER}
|
|
416
|
+
|
|
417
|
+
On reaching terminal state (workflow:merged, workflow:invalid, needs-human, or workflow:awaiting-merge), post
|
|
418
|
+
<!-- FORGE:CLAIM_RELEASED --> on the coordination issue to release your claim.
|
|
419
|
+
|
|
420
|
+
Set FORGE_COORD_ISSUE=${FORGE_COORD_ISSUE} in your environment so /work-on phases can read it."
|
|
421
|
+
fi
|
|
422
|
+
```
|
|
423
|
+
|
|
300
424
|
**Hot-copy CONTRACT context** (extends `{GIST_CONTEXT}` for milestone-lane issues where the parent issue already carries a `FORGE:CONTRACT` annotation): <!-- Added: forge#1277 -->
|
|
301
425
|
|
|
302
426
|
When a DAG node issue was spawned from a decomposition (parent issue has `workflow:decomposed` label and the child issue body references `**Parent**: #NNN`), the parent issue may already have a `FORGE:CONTRACT` annotation that was posted before decomposition. Inject a scoped excerpt into the child's prompt so the child does not re-fetch it.
|
|
@@ -339,23 +463,71 @@ You will be automatically notified when each background agent completes. **Do NO
|
|
|
339
463
|
|
|
340
464
|
**Successor dispatch latency is measured from `agent_completed`, not from orchestrator polling.** The moment you receive an `agent_completed` notification for issue N, that is t=0 for dispatching N's successors. Any successor whose predecessors are all now terminal MUST be dispatched in the same response that processes the notification — not after a poll cycle, not after a sleep. This is the design property that makes streaming DAG execution faster than wave-based execution. <!-- Added: forge#1251 -->
|
|
341
465
|
|
|
342
|
-
**
|
|
466
|
+
**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:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
classify_predecessor_state() {
|
|
470
|
+
local PRED="$1"
|
|
471
|
+
local PRED_INFO
|
|
472
|
+
# NOTE: the `workflow` array below deliberately also keeps `needs-human` — that label has NO
|
|
473
|
+
# `workflow:` prefix (see bin/labels.json), so a bare `select(startswith("workflow:"))` would
|
|
474
|
+
# drop it and the GATED branch's `needs-human` case would be dead code (forge#1812 primary case).
|
|
475
|
+
PRED_INFO=$(gh issue view "$PRED" -R {GH_REPO} --json labels,state \
|
|
476
|
+
--jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:") or . == "needs-human")]}' 2>/dev/null || echo '{}')
|
|
477
|
+
local PRED_STATE PRED_LABELS
|
|
478
|
+
PRED_STATE=$(echo "$PRED_INFO" | jq -r '.state // "OPEN"')
|
|
479
|
+
PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
|
|
480
|
+
|
|
481
|
+
if echo "$PRED_LABELS" | grep -qx "workflow:invalid"; then
|
|
482
|
+
echo "FAILED"
|
|
483
|
+
elif echo "$PRED_LABELS" | grep -qx "workflow:merged"; then
|
|
484
|
+
echo "DONE"
|
|
485
|
+
elif echo "$PRED_LABELS" | grep -qxE "needs-human|workflow:awaiting-merge"; then
|
|
486
|
+
echo "GATED"
|
|
487
|
+
elif [ "$PRED_STATE" = "CLOSED" ]; then
|
|
488
|
+
# Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
|
|
489
|
+
# not a new deadlock state; there is no pending code for dependents to wait on.
|
|
490
|
+
echo "DONE"
|
|
491
|
+
else
|
|
492
|
+
echo "IN_PROGRESS"
|
|
493
|
+
fi
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- **DONE** — predecessor's code is in the base branch (`workflow:merged`), or the predecessor is closed with no pending code. Safe for dependents to dispatch.
|
|
498
|
+
- **GATED** — predecessor is paused pending a human decision (`needs-human`) or pending only a human merge click (`workflow:awaiting-merge`). Its code is NOT yet in the base branch. Dependents are neither dispatched nor skipped — they move to the `blocked-on-human-merge` tracked state (item 6.5 below).
|
|
499
|
+
- **FAILED** — predecessor was closed as `workflow:invalid`, or the agent explicitly reported a build/test error. Dependents are marked "skipped — dependency failed" (item 6 below) — unchanged from prior behavior.
|
|
500
|
+
- **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`). Dependent simply continues waiting; no special tracking needed.
|
|
501
|
+
|
|
502
|
+
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
|
+
|
|
504
|
+
**Core streaming dispatch loop**: After processing each agent completion, check the DAG for newly unblocked issues. If any issue now has all predecessors classified `DONE`, dispatch it immediately (run Steps 4A.pre.0, 4A.pre, and 4A for the newly ready issues). This is the key difference from the wave model — issues dispatch as soon as their specific predecessors complete, not after an entire group finishes.
|
|
343
505
|
|
|
344
506
|
```bash
|
|
345
507
|
# After each agent completion, check for newly ready issues:
|
|
346
508
|
for BLOCKED_NUM in {all_blocked_issue_numbers}; do
|
|
347
509
|
ALL_PREDS_DONE=true
|
|
510
|
+
ANY_PRED_GATED=false
|
|
511
|
+
GATING_PREDS=()
|
|
348
512
|
for PRED in {predecessors_of_BLOCKED_NUM}; do
|
|
349
|
-
PRED_STATE=$(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
ALL_PREDS_DONE=false
|
|
353
|
-
|
|
354
|
-
|
|
513
|
+
PRED_STATE=$(classify_predecessor_state "$PRED")
|
|
514
|
+
case "$PRED_STATE" in
|
|
515
|
+
DONE) ;; # satisfied — no action
|
|
516
|
+
FAILED) ALL_PREDS_DONE=false ;; # handled by item 6 (skip dependents)
|
|
517
|
+
GATED)
|
|
518
|
+
ALL_PREDS_DONE=false
|
|
519
|
+
ANY_PRED_GATED=true
|
|
520
|
+
GATING_PREDS+=("$PRED")
|
|
521
|
+
;; # handled by item 6.5 (blocked-on-human-merge)
|
|
522
|
+
IN_PROGRESS|*) ALL_PREDS_DONE=false ;; # just keep waiting
|
|
523
|
+
esac
|
|
355
524
|
done
|
|
356
525
|
if [ "$ALL_PREDS_DONE" = "true" ]; then
|
|
357
|
-
echo "#{BLOCKED_NUM} is now READY — all predecessors resolved. Dispatching."
|
|
526
|
+
echo "#{BLOCKED_NUM} is now READY — all predecessors DONE (merged/resolved). Dispatching."
|
|
358
527
|
# Add to dispatch batch for this completion cycle
|
|
528
|
+
elif [ "$ANY_PRED_GATED" = "true" ]; then
|
|
529
|
+
echo "#{BLOCKED_NUM} is BLOCKED-ON-HUMAN-MERGE — gated by: ${GATING_PREDS[*]}. See item 6.5."
|
|
530
|
+
# Do NOT dispatch. Do NOT mark skipped. Tracked via item 6.5.
|
|
359
531
|
fi
|
|
360
532
|
done
|
|
361
533
|
# Run Steps 4A.pre.0 → 4A.pre → 4A for newly ready issues (batch them in a single message)
|
|
@@ -365,12 +537,16 @@ done
|
|
|
365
537
|
|
|
366
538
|
1. **Check if the agent completed the FULL pipeline** — not just one phase:
|
|
367
539
|
```bash
|
|
368
|
-
# Check final workflow state —
|
|
540
|
+
# Check final workflow state — workflow:merged, workflow:invalid, needs-human, and
|
|
541
|
+
# workflow:awaiting-merge all mean this agent's own /work-on run has stopped (the last
|
|
542
|
+
# two are human-gated pauses, not completions). See Predecessor Classification above for
|
|
543
|
+
# how the DAG's readiness/failure logic treats these same labels differently from "this
|
|
544
|
+
# agent is done running."
|
|
369
545
|
FINAL_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels,state --jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}')
|
|
370
546
|
echo "#{NUM}: $FINAL_STATE"
|
|
371
547
|
```
|
|
372
548
|
|
|
373
|
-
2. **If the issue is NOT in a terminal state** (`workflow:merged`, `workflow:invalid`,
|
|
549
|
+
2. **If the issue is NOT in a terminal-for-this-agent state** (`workflow:merged`, `workflow:invalid`, `needs-human`, or `workflow:awaiting-merge`), the agent stalled mid-pipeline. **Resume it immediately**:
|
|
374
550
|
```
|
|
375
551
|
Agent(
|
|
376
552
|
resume=AGENT_ISSUE_MAP[{NUMBER}],
|
|
@@ -383,11 +559,100 @@ done
|
|
|
383
559
|
|
|
384
560
|
3. **Track resume cycles per agent.** If an agent has been resumed 2+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
|
|
385
561
|
|
|
386
|
-
4. **
|
|
562
|
+
4. **Post CLAIM_RELEASED on coordination issue** (when `FORGE_COORD_ISSUE` is set): <!-- Added: forge#1736 -->
|
|
563
|
+
```bash
|
|
564
|
+
# After verifying terminal state for issue NUM, release its claim on the coordination issue
|
|
565
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
|
|
566
|
+
COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
|
|
567
|
+
if [ -n "$COORD_NUM" ]; then
|
|
568
|
+
gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM_RELEASED -->
|
|
569
|
+
**Holder**: #${NUM} — reached terminal state: ${FINAL_WORKFLOW_STATE}
|
|
570
|
+
**Released**: $(date -u +%Y-%m-%dT%H:%M:%SZ)" 2>/dev/null || true
|
|
571
|
+
echo "CLAIM_RELEASED posted for #${NUM} on coordination issue #${COORD_NUM}"
|
|
572
|
+
fi
|
|
573
|
+
fi
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Claims-board relaxation sweep** (run after posting CLAIM_RELEASED): When a claim is released, check all remaining Layer-2/4-serialized issue pairs. If the now-released Holder's claimed files were the *only* conflict reason for a still-blocked issue, and that blocked issue already has an active `FORGE:CLAIM` with a disjoint file set, the blocking edge MAY be relaxed (blocked issue becomes ready). <!-- Added: forge#1736 -->
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# After CLAIM_RELEASED for issue NUM:
|
|
580
|
+
# Read all active FORGE:CLAIM annotations from coordination issue
|
|
581
|
+
if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
|
|
582
|
+
ACTIVE_CLAIMS=$(gh api repos/{GH_REPO}/issues/${COORD_NUM}/comments \
|
|
583
|
+
--jq '[.[] | select(.body | contains("<!-- FORGE:CLAIM -->")) |
|
|
584
|
+
select(.body | contains("<!-- FORGE:CLAIM_RELEASED -->") | not)] |
|
|
585
|
+
map({holder: (.body | capture("\\*\\*Holder\\*\\*: (?P<h>[^\\n]+)").h),
|
|
586
|
+
files: (.body | capture("\\*\\*Files\\*\\*: (?P<f>[^\\n]+)").f)})' 2>/dev/null || echo '[]')
|
|
587
|
+
# For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
|
|
588
|
+
# is disjoint from all remaining active claims. If so, mark it ready.
|
|
589
|
+
# (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
|
|
590
|
+
for BLOCKED_NUM in {layer_2_4_blocked_issues}; do
|
|
591
|
+
BLOCKED_CLAIM_FILES=$(echo "$ACTIVE_CLAIMS" \
|
|
592
|
+
| jq -r --arg h "#${BLOCKED_NUM}" '.[] | select(.holder | startswith($h)) | .files' 2>/dev/null || echo "")
|
|
593
|
+
if [ -n "$BLOCKED_CLAIM_FILES" ]; then
|
|
594
|
+
# Compare file set with all other active claims — if disjoint, downgrade to ready
|
|
595
|
+
echo "Claims-board relaxation: checking if #${BLOCKED_NUM} can be unblocked based on disjoint claims"
|
|
596
|
+
# (Implementer: build a set-intersection check here using sorted file lists)
|
|
597
|
+
fi
|
|
598
|
+
done
|
|
599
|
+
fi
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
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
|
|
387
603
|
|
|
388
604
|
5. **Check for newly unblocked issues** — run the DAG readiness check above. If any issues are now ready, dispatch them immediately (Steps 4A.pre.0 → 4A.pre → 4A). Batch all newly ready issues into a single dispatch message.
|
|
389
605
|
|
|
390
|
-
6. **Handle predecessor failures** — if a completed agent's issue FAILED (needs-human
|
|
606
|
+
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. Mark all transitive dependents as "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
|
|
607
|
+
|
|
608
|
+
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`:
|
|
609
|
+
```bash
|
|
610
|
+
# Resolve PRED's open PR, if any, using the anchored search (forge#1634/#1646 precedent —
|
|
611
|
+
# do NOT fall back to a bare-number search here; a stale unrelated PR would misattribute gating).
|
|
612
|
+
GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
|
|
613
|
+
--json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
614
|
+
PRED_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
|
|
615
|
+
--jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // "needs-human"' 2>/dev/null)
|
|
616
|
+
|
|
617
|
+
# Self-heal the label if it hasn't been bootstrapped yet (same pattern as review-pr.md 6C —
|
|
618
|
+
# colors match the canonical manifest bin/labels.json; --force makes this idempotent/cheap).
|
|
619
|
+
gh label create "blocked-on-human-merge" --color "006B75" --description "Dependent of a gated (needs-human/awaiting-merge) predecessor. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
620
|
+
|
|
621
|
+
# Idempotency: only post/label if not already tracked for this specific predecessor.
|
|
622
|
+
# Anchor on the exact "**Gating predecessor**: #N" label with a word boundary —
|
|
623
|
+
# a bare contains("#N") substring would false-match #50/#500 for predecessor #5. <!-- forge#1830 -->
|
|
624
|
+
ALREADY_TRACKED=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
625
|
+
--jq --arg prednum "${PRED}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
|
|
626
|
+
if [ "$ALREADY_TRACKED" -eq 0 ]; then
|
|
627
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
|
|
628
|
+
**Gating predecessor**: #${PRED} (state: \`${PRED_LABEL}\`${GATING_PR:+, open PR #${GATING_PR}})
|
|
629
|
+
**Status**: This issue is ready to dispatch as soon as #${PRED}'s gating PR merges. No action needed — the orchestrator (live session via item 6.6, or the next \`/orchestrate\` invocation via phase-3-dependency.md's wake reconstruction) will auto-dispatch it the moment #${PRED} reaches \`workflow:merged\`."
|
|
630
|
+
gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
|
|
631
|
+
fi
|
|
632
|
+
```
|
|
633
|
+
Do NOT dispatch `DEP`. Do NOT mark it skipped — it remains visibly tracked as `blocked-on-human-merge` in the DAG, re-evaluated on the next completion event, stall-detection pass, or session wake.
|
|
634
|
+
|
|
635
|
+
6.6. **Merge-triggered wake for blocked-on-human-merge dependents** <!-- Added: forge#1812 --> — whenever a completed agent's issue classifies as `DONE` via `workflow:merged` (i.e. it just merged), check whether any other issue is tracked as blocked on it — this makes gated dependents dispatch the instant the gating PR merges, with no manual `/orchestrate` re-run required:
|
|
636
|
+
```bash
|
|
637
|
+
WOKEN=$(gh issue list -R {GH_REPO} --state open --label "blocked-on-human-merge" --json number \
|
|
638
|
+
--jq '.[].number' 2>/dev/null || echo "")
|
|
639
|
+
for DEP in $WOKEN; do
|
|
640
|
+
IS_GATED_BY_THIS=$(gh api repos/{GH_REPO}/issues/${DEP}/comments \
|
|
641
|
+
--jq --arg prednum "${NUM}" '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE") and test("Gating predecessor\\*\\*: #" + $prednum + "\\b"))] | length' 2>/dev/null || echo "0")
|
|
642
|
+
[ "$IS_GATED_BY_THIS" -gt 0 ] || continue
|
|
643
|
+
# Idempotency: only dispatch if DEP hasn't already been dispatched by another path.
|
|
644
|
+
DEP_ALREADY_DISPATCHED=$(gh issue view "$DEP" -R {GH_REPO} --json labels \
|
|
645
|
+
--jq '[.labels[].name | select(startswith("workflow:"))] | length' 2>/dev/null || echo "0")
|
|
646
|
+
if [ "$DEP_ALREADY_DISPATCHED" -eq 0 ]; then
|
|
647
|
+
gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
|
|
648
|
+
gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
|
|
649
|
+
Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was tracked via a prior FORGE:BLOCKED_ON_HUMAN_MERGE comment.)"
|
|
650
|
+
echo "#{DEP} unblocked by #{NUM} merge — dispatching immediately (Steps 4A.pre.0 → 4A.pre → 4A)."
|
|
651
|
+
# Add DEP to the same-response dispatch batch
|
|
652
|
+
fi
|
|
653
|
+
done
|
|
654
|
+
```
|
|
655
|
+
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.
|
|
391
656
|
|
|
392
657
|
7. **Verify pipeline compliance** — for each truly completed issue, check that the agent used `/work-on`:
|
|
393
658
|
```bash
|
|
@@ -404,13 +669,22 @@ done
|
|
|
404
669
|
✓ #{NUMBER} — {title} → PR #{PR} merged to {target}
|
|
405
670
|
✗ #{NUMBER} — {title} → {reason for failure}
|
|
406
671
|
⚠ #{NUMBER} — {title} → PIPELINE BYPASS (no /work-on — PR invalid)
|
|
672
|
+
⏸ #{NUMBER} — {title} → PR #{PR} awaiting-merge (remediated + re-approved — human merge only, no diagnosis needed)
|
|
673
|
+
🔗 #{NUMBER} — {title} → blocked-on-human-merge (gated by #{PRED}, will auto-dispatch on #{PRED} merge)
|
|
407
674
|
⏳ Progress: {completed}/{total} complete, {active} active, {blocked} blocked
|
|
408
675
|
→ Dispatched #{NEWLY_READY} (predecessor #{PRED} completed)
|
|
409
676
|
```
|
|
410
677
|
|
|
678
|
+
`⏸` (awaiting-merge) is deliberately distinct from `⚠` (blocked/bypass) — do not collapse the
|
|
679
|
+
two. `⚠` means the pipeline hit something it cannot resolve and a human must diagnose it;
|
|
680
|
+
`⏸` means the PR already cleared re-review and only needs a merge click. `🔗` (blocked-on-human-merge,
|
|
681
|
+
forge#1812) is distinct from both: it marks a DEPENDENT of a GATED predecessor (item 6.5) — the
|
|
682
|
+
dependent itself has no problem at all, it is simply waiting on someone else's merge. See Phase 6's
|
|
683
|
+
"Merge-Ready" report section (`phase-6-report.md` Step 6A.5/6B) for the batch-level rollup.
|
|
684
|
+
|
|
411
685
|
9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
|
|
412
686
|
|
|
413
|
-
**Termination condition**: All issues in the DAG
|
|
687
|
+
**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.
|
|
414
688
|
|
|
415
689
|
**Anti-pattern — DO NOT DO THIS:**
|
|
416
690
|
- `sleep 60/120/180/300` loops to check status — you will be notified automatically
|
|
@@ -437,9 +711,12 @@ STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/nul
|
|
|
437
711
|
**For each non-terminal agent in the current batch**:
|
|
438
712
|
```bash
|
|
439
713
|
for NUM in {active_issue_numbers}; do
|
|
440
|
-
# Skip issues already in terminal state
|
|
714
|
+
# Skip issues already in a terminal-for-this-agent state (merged/invalid/needs-human/awaiting-merge).
|
|
715
|
+
# workflow:awaiting-merge MUST be included here (forge#1812) — otherwise the stall detector
|
|
716
|
+
# re-escalates an already-remediated-and-re-approved PR back to needs-human after STALL_TIMEOUT,
|
|
717
|
+
# silently collapsing the Awaiting-Merge/Blocked distinction forge#1811 introduced.
|
|
441
718
|
TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
|
|
442
|
-
--jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
|
|
719
|
+
--jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human" or . == "workflow:awaiting-merge")] | length')
|
|
443
720
|
[ "$TERMINAL" -gt 0 ] && continue
|
|
444
721
|
|
|
445
722
|
# Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
|
|
@@ -607,6 +884,114 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
607
884
|
done
|
|
608
885
|
```
|
|
609
886
|
|
|
887
|
+
**Surface-area batching for queued P3 findings (MANDATORY check before dispatch):** <!-- Added: forge#1818 -->
|
|
888
|
+
|
|
889
|
+
Cascade-spawned findings collected within a single `/orchestrate` run never pass back through Phase 1's batching rule — Phase 1 only runs once, at the start. Without a check here, same-file P3 findings spawned mid-run always dispatch individually, defeating the batching policy for exactly the findings it exists to catch. Apply the same grouping rule from `commands/orchestrate/phase-1-resolve.md` ("P3 Review-Finding Batching") to `QUEUED_FINDINGS` before the dispatch step below:
|
|
890
|
+
|
|
891
|
+
```bash
|
|
892
|
+
# Group QUEUED_FINDINGS by exact affected file, reusing the SAME safety
|
|
893
|
+
# exclusions as phase-1-resolve.md. Both sites go through jq test() (Oniguruma)
|
|
894
|
+
# with identical patterns — NOT grep ERE — so the two batching checks cannot
|
|
895
|
+
# classify the same issue body differently. <!-- forge#1837 -->
|
|
896
|
+
declare -A SURFACE_FILE_MEMBERS
|
|
897
|
+
SURFACE_BATCHED_FINDINGS=()
|
|
898
|
+
|
|
899
|
+
# Defensive cap on gh issue view fan-out. QUEUED_FINDINGS is already bounded by
|
|
900
|
+
# upstream cascade control; this cap holds even if that bound is later loosened,
|
|
901
|
+
# so the loop can never scale API calls linearly with cascade-seeded findings. <!-- forge#1836 -->
|
|
902
|
+
MAX_BATCH_SCAN=50
|
|
903
|
+
SCANNED=0
|
|
904
|
+
|
|
905
|
+
for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
906
|
+
SCANNED=$((SCANNED + 1))
|
|
907
|
+
if [ "$SCANNED" -gt "$MAX_BATCH_SCAN" ]; then
|
|
908
|
+
echo "Surface-area batching: reached MAX_BATCH_SCAN=$MAX_BATCH_SCAN — remaining findings stay individually queued"
|
|
909
|
+
break
|
|
910
|
+
fi
|
|
911
|
+
|
|
912
|
+
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json title,body,labels \
|
|
913
|
+
--jq '{title: .title, body: .body, labels: [.labels[].name]}')
|
|
914
|
+
|
|
915
|
+
# Safety exclusions — never batch, at any priority. Same jq test() engine and
|
|
916
|
+
# patterns as phase-1-resolve.md's batching rule (single shared mechanism).
|
|
917
|
+
echo "$FINDING_DATA" | jq -e '
|
|
918
|
+
(.title | test("security|billing|anti-bot|auth"; "i"))
|
|
919
|
+
or (.body | test("## Problem[\\s\\S]{0,500}(security|billing|anti-bot|auth)"; "i"))
|
|
920
|
+
or ([.labels[]] | any(. == "security" or . == "billing" or . == "anti-bot" or . == "auth"))
|
|
921
|
+
' >/dev/null && continue
|
|
922
|
+
|
|
923
|
+
# Only P3 findings are eligible (P1/P2 already dispatched individually above)
|
|
924
|
+
echo "$FINDING_DATA" | jq -e '[.labels[]] | any(. == "priority:P3")' >/dev/null || continue
|
|
925
|
+
|
|
926
|
+
FINDING_FILE=$(echo "$FINDING_DATA" | jq -r '.body' | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|sh|md)`' | head -1 | tr -d '`')
|
|
927
|
+
[ -z "$FINDING_FILE" ] && continue
|
|
928
|
+
|
|
929
|
+
SURFACE_FILE_MEMBERS["$FINDING_FILE"]="${SURFACE_FILE_MEMBERS[$FINDING_FILE]} $FINDING_NUM"
|
|
930
|
+
done
|
|
931
|
+
|
|
932
|
+
# For each same-file cluster of 2+, actually CREATE the batch issue (executable —
|
|
933
|
+
# mirrors phase-1-resolve.md's "Batch creation rule") and REPLACE the members with
|
|
934
|
+
# the batch issue in QUEUED_FINDINGS so the dispatch step below never double-dispatches
|
|
935
|
+
# them. This is what makes SURFACE_BATCHED_FINDINGS a live control, not dead wiring. <!-- forge#1832, forge#1834 -->
|
|
936
|
+
for FILE in "${!SURFACE_FILE_MEMBERS[@]}"; do
|
|
937
|
+
MEMBERS=(${SURFACE_FILE_MEMBERS[$FILE]})
|
|
938
|
+
[ "${#MEMBERS[@]}" -ge 2 ] || continue
|
|
939
|
+
|
|
940
|
+
# Sanitize the affected-file path before interpolating it into the issue title/body.
|
|
941
|
+
# Git filenames can legally carry shell metacharacters (`$()`, backticks, quotes);
|
|
942
|
+
# restrict to a validated charset so the value cannot break the gh argument
|
|
943
|
+
# boundary from an untrusted issue body. Shared guard with phase-1-resolve.md. <!-- forge#1833, forge#1835 -->
|
|
944
|
+
SAFE_SURFACE_AREA=$(printf '%s' "$FILE" | tr -cd 'A-Za-z0-9._/-')
|
|
945
|
+
|
|
946
|
+
echo "Same-run surface-area cluster: ${#MEMBERS[@]} P3 findings share $FILE — creating batch issue(s)"
|
|
947
|
+
|
|
948
|
+
# Cap at 8 members per batch (phase-1-resolve.md limit); split into batches of <=8.
|
|
949
|
+
for START in $(seq 0 8 $(( ${#MEMBERS[@]} - 1 ))); do
|
|
950
|
+
CHUNK=("${MEMBERS[@]:$START:8}")
|
|
951
|
+
[ "${#CHUNK[@]}" -ge 2 ] || continue
|
|
952
|
+
|
|
953
|
+
MEMBER_LINES=""
|
|
954
|
+
for M in "${CHUNK[@]}"; do
|
|
955
|
+
MTITLE=$(gh issue view "$M" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
|
|
956
|
+
MEMBER_LINES="${MEMBER_LINES}- [ ] #${M}: ${MTITLE}"$'\n'
|
|
957
|
+
done
|
|
958
|
+
|
|
959
|
+
BATCH_ISSUE_NUM=$(gh issue create {GH_FLAG} \
|
|
960
|
+
--title "fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (same-run batch)" \
|
|
961
|
+
--label "review-finding,priority:P3,batch" \
|
|
962
|
+
--body "$(cat <<BATCH_EOF
|
|
963
|
+
## Problem
|
|
964
|
+
|
|
965
|
+
Batch of P3 review findings in **${SAFE_SURFACE_AREA}** (same file), clustered mid-run by phase-4-execution.md to reduce per-finding pipeline overhead.
|
|
966
|
+
|
|
967
|
+
## Member Findings
|
|
968
|
+
|
|
969
|
+
<!-- FORGE:BATCH_MEMBERS -->
|
|
970
|
+
${MEMBER_LINES}<!-- /FORGE:BATCH_MEMBERS -->
|
|
971
|
+
|
|
972
|
+
## Acceptance Criteria
|
|
973
|
+
|
|
974
|
+
- [ ] All member findings addressed or closed as false-positive
|
|
975
|
+
- [ ] Member issues auto-closed with reference to this batch PR on merge
|
|
976
|
+
- [ ] No security, billing, anti-bot, or auth paths touched (validated before batching)
|
|
977
|
+
|
|
978
|
+
<!-- FORGE:BATCHABLE -->
|
|
979
|
+
BATCH_EOF
|
|
980
|
+
)" --json number --jq '.number')
|
|
981
|
+
|
|
982
|
+
# Consume the cluster: record members and REPLACE them in QUEUED_FINDINGS
|
|
983
|
+
# with the single batch issue, so the dispatch step below operates on the
|
|
984
|
+
# batch unit and skips the individual members.
|
|
985
|
+
SURFACE_BATCHED_FINDINGS+=("${CHUNK[@]}")
|
|
986
|
+
QUEUED_FINDINGS=($(printf '%s\n' "${QUEUED_FINDINGS[@]}" | grep -vxF -f <(printf '%s\n' "${CHUNK[@]}") || true))
|
|
987
|
+
[ -n "$BATCH_ISSUE_NUM" ] && QUEUED_FINDINGS+=("$BATCH_ISSUE_NUM")
|
|
988
|
+
echo "Batched ${#CHUNK[@]} findings into #${BATCH_ISSUE_NUM}; members removed from QUEUED_FINDINGS and the DAG."
|
|
989
|
+
done
|
|
990
|
+
done
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
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
|
+
|
|
610
995
|
**For queued (non-deferred) findings:**
|
|
611
996
|
|
|
612
997
|
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.
|
|
@@ -761,11 +1146,13 @@ fi
|
|
|
761
1146
|
### Step 4E: Handle individual agent failures
|
|
762
1147
|
|
|
763
1148
|
If an agent reports failure or error:
|
|
764
|
-
- **Merge conflict**: Report to user, mark issue as needing human attention
|
|
765
|
-
- **Invalid issue**: Already handled by the agent (closed with comment) — just report it
|
|
766
|
-
- **Build/test failure**: Report the error, suggest manual intervention
|
|
767
|
-
- **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`
|
|
768
|
-
- **Dependency cascade
|
|
1149
|
+
- **Merge conflict**: Report to user, mark issue as needing human attention (`needs-human`). This classifies as **GATED**, not FAILED — see Predecessor Classification in Step 4B. Its dependents follow the Dependency cascade rule below, not a hard skip.
|
|
1150
|
+
- **Invalid issue**: Already handled by the agent (closed with comment) — just report it. This classifies as **FAILED**.
|
|
1151
|
+
- **Build/test failure**: Report the error, suggest manual intervention. This classifies as **FAILED**.
|
|
1152
|
+
- **Agent timeout**: Report which issue timed out, suggest re-running with `/work-on #{N}`. Not yet terminal — leave dependents in `IN_PROGRESS` wait, no cascade action.
|
|
1153
|
+
- **Dependency cascade** <!-- Updated: forge#1812 -->: Re-run `classify_predecessor_state` (Step 4B) for the failed/gated issue before cascading — do NOT assume every entry above is a hard failure:
|
|
1154
|
+
- If it classifies **FAILED** (invalid, or build/test failure): mark all transitive dependents in the DAG as "skipped — dependency #{X} failed" (same as Step 4B item 6).
|
|
1155
|
+
- If it classifies **GATED** (merge conflict → `needs-human`, or `workflow:awaiting-merge`): do NOT mark dependents skipped. Instead apply Step 4B item 6.5 — track each direct dependent as `blocked-on-human-merge` against this predecessor, so it auto-dispatches via item 6.6 the moment the predecessor reaches `workflow:merged`.
|
|
769
1156
|
|
|
770
1157
|
**Do NOT retry failed agents automatically.** Report the failure and let the user decide.
|
|
771
1158
|
|
|
@@ -921,7 +1308,7 @@ The context-gathering phase can fetch this index to discover all investigation G
|
|
|
921
1308
|
|
|
922
1309
|
**YOUR MISSION**: Invoke \`/work-on\` via the Skill tool and let it run to completion. \`/work-on\` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — \`/work-on\` handles everything including issue closure and label updates in its close phase.
|
|
923
1310
|
|
|
924
|
-
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, or \`
|
|
1311
|
+
**CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, \`needs-human\`, or \`workflow:awaiting-merge\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
|
|
925
1312
|
|
|
926
1313
|
**HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with \`review-finding\` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
|
|
927
1314
|
|
|
@@ -943,7 +1330,7 @@ After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the
|
|
|
943
1330
|
gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
|
|
944
1331
|
\`\`\`
|
|
945
1332
|
**Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
|
|
946
|
-
**Terminal condition also**: \`needs-human\` label present OR issue state is \`closed\`
|
|
1333
|
+
**Terminal condition also**: \`needs-human\` label present, \`workflow:awaiting-merge\` label present, OR issue state is \`closed\`
|
|
947
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.
|
|
948
1335
|
|
|
949
1336
|
**CRITICAL — SOURCE BRANCH DETECTION**:
|
|
@@ -975,12 +1362,44 @@ Completion Sweep Results:
|
|
|
975
1362
|
Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
|
|
976
1363
|
```
|
|
977
1364
|
|
|
978
|
-
**After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
|
|
1365
|
+
**After sweep agents complete** (or if no findings were dispatched): output the budget deferred-issues report (if applicable), then proceed to Phase 5.
|
|
979
1366
|
|
|
980
1367
|
**Anti-patterns — DO NOT DO THIS:**
|
|
981
1368
|
- Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
|
|
982
1369
|
- Overriding generation >= 2 deferrals — the cascade cap is absolute.
|
|
983
1370
|
- Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
|
|
984
1371
|
|
|
1372
|
+
### Step 4F.5: Budget Deferred-Issues Report (conditional) <!-- Added: forge#1743 -->
|
|
1373
|
+
|
|
1374
|
+
**Run only when `BUDGET_LIMIT != "Infinity"` AND `${#DEFERRED_BUDGET_ISSUES[@]} > 0`.**
|
|
1375
|
+
|
|
1376
|
+
Deferred issues are issues that were not dispatched because their estimated cost would have pushed projected spend past the budget ceiling (after reserving ε for no-prior issues). They are **never silently dropped** — this report makes them visible and actionable.
|
|
1377
|
+
|
|
1378
|
+
```bash
|
|
1379
|
+
if [ "$BUDGET_LIMIT" != "Infinity" ] && [ "${#DEFERRED_BUDGET_ISSUES[@]}" -gt 0 ]; then
|
|
1380
|
+
echo ""
|
|
1381
|
+
echo "## Budget Report"
|
|
1382
|
+
echo ""
|
|
1383
|
+
echo "**Budget limit**: \$${BUDGET_LIMIT}"
|
|
1384
|
+
echo "**Projected spend (dispatched issues)**: \$${PROJECTED_SPEND}"
|
|
1385
|
+
echo "**Actual spend (completed issues, best-effort)**: \$${ACTUAL_SPEND}"
|
|
1386
|
+
echo "**ε-reserve used**: ${EPSILON_DISPATCHED} (10% = \$${EPSILON_BUDGET})"
|
|
1387
|
+
echo ""
|
|
1388
|
+
echo "### Deferred Issues (budget exhausted — never silently dropped)"
|
|
1389
|
+
echo ""
|
|
1390
|
+
echo "| Issue | Title | Score | Est. Cost | Reason |"
|
|
1391
|
+
echo "|-------|-------|-------|-----------|--------|"
|
|
1392
|
+
for DNUM in "${DEFERRED_BUDGET_ISSUES[@]}"; do
|
|
1393
|
+
DTITLE=$(gh issue view "$DNUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "(unknown)")
|
|
1394
|
+
DSCORE="${ISSUE_SCORE[$DNUM]:-?}"
|
|
1395
|
+
DCOST="${ISSUE_COST_ESTIMATE[$DNUM]:-?}"
|
|
1396
|
+
echo "| #${DNUM} | ${DTITLE} | ${DSCORE} | \$${DCOST} | Budget ceiling reached |"
|
|
1397
|
+
done
|
|
1398
|
+
echo ""
|
|
1399
|
+
DEFERRED_LIST=$(IFS=' '; echo "${DEFERRED_BUDGET_ISSUES[*]}")
|
|
1400
|
+
echo "**Action**: Re-run \`/orchestrate ${DEFERRED_LIST} [--budget N]\` to process deferred issues, or increase \`--budget\`."
|
|
1401
|
+
fi
|
|
1402
|
+
```
|
|
1403
|
+
|
|
985
1404
|
---
|
|
986
1405
|
|