forgedock 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/bin/engine/orchestrate-canary.mjs +15 -0
- package/bin/engine/orchestrate-canary.test.mjs +28 -0
- package/bin/forgedock.mjs +91 -21
- package/bin/hooks/session-start.mjs +22 -1
- package/bin/opencode-adapter.mjs +435 -0
- package/bin/registry.mjs +87 -0
- package/bin/runner.mjs +354 -43
- package/commands/orchestrate/phase-3-dependency.md +29 -4
- package/commands/orchestrate/phase-4-execution.md +197 -26
- package/commands/work-on/build.md +10 -1
- package/commands/work-on.md +10 -1
- package/docs/OPENCODE.md +132 -252
- package/package.json +3 -2
- package/scripts/check-spec-markers.sh +1 -0
|
@@ -352,14 +352,39 @@ This step is the deterministic counterpart to fix #2 (atomic create-if-absent in
|
|
|
352
352
|
Before building agent prompts, run `classify-lane.sh` for every issue in the current dispatch group to compute `{LANE}` and `{PR_BASE}` deterministically. The script output is authoritative — the LLM MUST NOT override or reason around it.
|
|
353
353
|
|
|
354
354
|
```bash
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
# Resolve the helper once and reuse it for the initial dispatch and all finding
|
|
356
|
+
# classification loops. Claude keeps its installed path as the default, while
|
|
357
|
+
# OpenCode and Codex can use ForgeDock's repository-local scripts.
|
|
358
|
+
resolve_classify_lane() {
|
|
359
|
+
local candidates=()
|
|
360
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
361
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
362
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
363
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
364
|
+
[ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
|
|
365
|
+
[ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
|
|
366
|
+
candidates+=("$HOME/.opencode/scripts/classify-lane.sh")
|
|
367
|
+
else
|
|
368
|
+
[ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
|
|
369
|
+
candidates+=("$HOME/.claude/scripts/classify-lane.sh")
|
|
370
|
+
[ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
local candidate
|
|
374
|
+
for candidate in "${candidates[@]}"; do
|
|
375
|
+
if [ -f "$candidate" ]; then
|
|
376
|
+
printf '%s\n' "$candidate"
|
|
377
|
+
return 0
|
|
378
|
+
fi
|
|
379
|
+
done
|
|
380
|
+
echo "ERROR: classify-lane.sh is not installed in any configured runtime path." >&2
|
|
381
|
+
return 1
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
CLASSIFY_LANE_SCRIPT=$(resolve_classify_lane) || {
|
|
385
|
+
echo "ERROR: cannot classify lanes without classify-lane.sh" >&2
|
|
386
|
+
exit 1
|
|
387
|
+
}
|
|
363
388
|
|
|
364
389
|
declare -A ISSUE_LANE
|
|
365
390
|
declare -A ISSUE_PR_BASE
|
|
@@ -498,7 +523,7 @@ SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch a
|
|
|
498
523
|
SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
|
|
499
524
|
|
|
500
525
|
for NUM in {ready_issue_numbers}; do
|
|
501
|
-
PR_BASE=$(bash
|
|
526
|
+
PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$NUM" -R {GH_REPO}) || {
|
|
502
527
|
echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
|
|
503
528
|
gh issue edit "$NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
504
529
|
continue
|
|
@@ -521,13 +546,57 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
|
|
|
521
546
|
|
|
522
547
|
**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.
|
|
523
548
|
|
|
549
|
+
**OpenCode dispatch (runtime-neutral adapter)**: When `FORGE_RUNTIME=opencode` or
|
|
550
|
+
an OpenCode runtime marker is present, do not invoke the engine's Claude-backed
|
|
551
|
+
runner and do not use the Claude `Agent(...)` fallback. Dispatch each ready
|
|
552
|
+
issue through OpenCode's native `task` tool, loading the nested
|
|
553
|
+
`commands/work-on.md` pipeline. Use GitHub labels and `FORGE:*` comments as the
|
|
554
|
+
resume state. After each task continuation, re-read the issue workflow label
|
|
555
|
+
and continue until it reaches
|
|
556
|
+
`workflow:merged`, `workflow:invalid`, `needs-human`, or
|
|
557
|
+
`workflow:awaiting-merge`.
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ]; then
|
|
561
|
+
echo "OpenCode native task dispatch selected — load commands/work-on.md for each ready issue."
|
|
562
|
+
echo "Do not call forgedock run-issue or the Claude Agent(...) fallback in this branch."
|
|
563
|
+
echo "If native task dispatch cannot run, post FORGE:OPENCODE_BLOCKED with the missing capability and add needs-human."
|
|
564
|
+
fi
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
This branch is additive. When OpenCode is not selected, continue through the
|
|
568
|
+
existing engine-first and Claude Agent-spawn paths below without modification.
|
|
569
|
+
|
|
524
570
|
**CRITICAL — never background via shell `&`/`wait`** (fixed forge#2466): A single `forgedock run-issue` invocation drives an issue through investigate → build → review → close and routinely runs 30+ minutes. Backgrounding the process at the *shell* level (`cmd &` … `wait`) does not escape the Bash tool's own per-invocation ceiling — `wait` is itself the foreground command the tool watches, and it blocks for the combined duration of every process in the chunk. This made engine-first dispatch effectively dead code: any chunk running longer than the ceiling was always killed, so every run silently fell through to the Agent-spawn fallback below. The fix: dispatch each `forgedock run-issue` invocation as its **own `Bash` tool call with `run_in_background=true`** — the harness's native "start it, don't wait, notify me on completion" primitive — never with shell-level `&`/`wait`. This is exactly the same async model the Agent-spawn-fallback path already uses (and Step 4B's notification-driven completion loop already expects), so one monitoring loop now covers both dispatch styles.
|
|
525
571
|
|
|
526
572
|
```bash
|
|
573
|
+
if [ "${FORGE_RUNTIME:-}" != "opencode" ] &&
|
|
574
|
+
[ -z "${OPENCODE_SESSION_ID:-}" ] &&
|
|
575
|
+
[ -z "${OPENCODE_PID:-}" ] &&
|
|
576
|
+
[ -z "${OPENCODE:-}" ]; then
|
|
527
577
|
# Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
|
|
528
578
|
# Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
|
|
529
579
|
FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
|
|
530
580
|
|
|
581
|
+
# Backend preflight canary (forge#2743): `command -v forgedock` only proves the orchestrator CLI
|
|
582
|
+
# binary is on PATH — it says nothing about whether the engine's execution backend (the `claude`
|
|
583
|
+
# CLI spawn, or a working `ANTHROPIC_API_KEY`) can actually run a phase. Without this probe, an
|
|
584
|
+
# environmental failure (e.g. forge#2741 — `spawnSync claude` ENOENT despite a shell `command -v`
|
|
585
|
+
# probe reporting the binary present) commits the ENTIRE ready set to engine-first dispatch, every
|
|
586
|
+
# `forgedock run-issue` call fails within seconds, and nothing re-routes the batch — a human has
|
|
587
|
+
# to notice and manually re-dispatch via the Agent-spawn path below.
|
|
588
|
+
#
|
|
589
|
+
# Use the runner's tested backend policy rather than duplicating it as shell/Node prose. This
|
|
590
|
+
# reuses the production resolved-CLI path check (including transient-shim refresh), honors an
|
|
591
|
+
# explicit FORGEDOCK_BACKEND, and accepts API fallback only when ANTHROPIC_API_KEY is configured.
|
|
592
|
+
# It performs no paid model request.
|
|
593
|
+
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
594
|
+
if ! forgedock backend-check --quiet; then
|
|
595
|
+
echo "WARNING: forgedock CLI is on PATH, but no configured execution backend is locally usable. Downgrading to the Agent-spawn fallback path for this entire run — see forge#2743."
|
|
596
|
+
FORGEDOCK_AVAILABLE="false"
|
|
597
|
+
fi
|
|
598
|
+
fi
|
|
599
|
+
|
|
531
600
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
532
601
|
# Build the budget-gated dispatch queue first (forge#1743's should_dispatch() still applies
|
|
533
602
|
# per-issue, independent of the concurrency cap below).
|
|
@@ -598,6 +667,7 @@ else
|
|
|
598
667
|
# Fall through to Agent-spawn template below. The SubagentStop hook (bin/hooks/interactive-engine.mjs)
|
|
599
668
|
# bridges these runs to the engine run-log for state persistence even on the fallback path.
|
|
600
669
|
fi
|
|
670
|
+
fi
|
|
601
671
|
```
|
|
602
672
|
|
|
603
673
|
**Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Issue one `Bash(...)` call per issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
|
|
@@ -849,7 +919,7 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle
|
|
|
849
919
|
|
|
850
920
|
You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. Both are the same underlying "background task finished" notification the harness delivers; treat them identically for the purposes of this step. **Do NOT use `sleep` loops to poll for completion.** Instead, wait for the automatic notification. When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for an `agent_completed` notification, `ENGINE_DISPATCH_MAP` (fixed forge#2466) for a background-Bash notification from the engine-first path — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them. **Fallback**: if a runtime ever fails to deliver a background-Bash completion notification, poll `gh issue view --json labels` for the in-flight engine-dispatched issue's workflow label instead of blocking — the same degrade-gracefully behavior already used when `BACKGROUND_DISPATCH_ENABLED=false` for the Agent-spawn path (see `phase-3-dependency.md` → Background Dispatch Mode).
|
|
851
921
|
|
|
852
|
-
**Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, or a background-Bash completion for an engine-dispatched issue — 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 —
|
|
922
|
+
**Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, or a background-Bash completion for an engine-dispatched issue — decrement `ACTIVE_DISPATCH_COUNT` by 1 — a worker slot has just freed, regardless of what terminal state the issue ended up in. Do this before any stall-recovery/resume logic below. If that agent is then resumed or fallback-dispatched (item 2 below, or Step 4B.5's stall recovery) because it stalled mid-pipeline rather than truly finishing, re-increment `ACTIVE_DISPATCH_COUNT` when the `Agent(resume=...)` (or, per item 2b below, the fresh `Agent(...)` fallback dispatch) call is issued — either re-occupies a worker slot exactly like a fresh dispatch does. (Step 4B.5's TIME-BASED hang detector — an agent that never completes at all, as opposed to one that completes at `workflow:engine-error` — remains `Agent(resume=...)`-specific and out of scope for this fix: an engine-dispatched issue that silently hangs still surfaces only via the standard stall-detection alert, and `forgedock resume-stalled` remains available as a separate manual/scripted recovery path for that case. The completion-triggered case — an engine-dispatched issue that DOES complete, at `workflow:engine-error` with empty committed state — is now auto-fallen-back to Agent-spawn by item 2b below, fixed forge#2743.)
|
|
853
923
|
|
|
854
924
|
**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.
|
|
855
925
|
|
|
@@ -888,6 +958,10 @@ classify_predecessor_state() {
|
|
|
888
958
|
# predecessor were still mid-pipeline — which is accurate, since the
|
|
889
959
|
# stall-detection step below (item 2) auto-resumes/retries a completed
|
|
890
960
|
# agent run that ends in workflow:engine-error, same as any other stall.
|
|
961
|
+
# Fixed forge#2743: item 2's sub-case 2b is what actually fulfills this for
|
|
962
|
+
# ENGINE_DISPATCH_MAP-tracked issues (fresh Agent-spawn fallback on empty
|
|
963
|
+
# committed state) — 2a's Agent(resume=...) alone cannot reach them, since
|
|
964
|
+
# they were never in AGENT_ISSUE_MAP.
|
|
891
965
|
echo "IN_PROGRESS"
|
|
892
966
|
elif [ "$PRED_STATE" = "CLOSED" ]; then
|
|
893
967
|
# Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
|
|
@@ -1086,7 +1160,11 @@ done
|
|
|
1086
1160
|
echo "#{NUM}: $FINAL_STATE"
|
|
1087
1161
|
```
|
|
1088
1162
|
|
|
1089
|
-
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**. `workflow:engine-error` (forge#2261 — `bin/engine.mjs`'s headless engine terminating on an engine/tool-level failure rather than a genuine human-judgment block) is deliberately excluded from the terminal-for-this-agent set: a completed agent run ending there is treated exactly like any other stall and auto-resumed/retried here, up to the resume-cap in item 3 below — no human decision is pending, so there is nothing to gate on
|
|
1163
|
+
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**. `workflow:engine-error` (forge#2261 — `bin/engine.mjs`'s headless engine terminating on an engine/tool-level failure rather than a genuine human-judgment block) is deliberately excluded from the terminal-for-this-agent set: a completed agent run ending there is treated exactly like any other stall and auto-resumed/retried here, up to the resume-cap in item 3 below — no human decision is pending, so there is nothing to gate on.
|
|
1164
|
+
|
|
1165
|
+
**Two distinct resume mechanisms, dispatched by which map resolved the completion (fixed forge#2743)** — `AGENT_ISSUE_MAP`-resolved completions (Agent-spawn path) and `ENGINE_DISPATCH_MAP`-resolved completions (engine-first path) hit `workflow:engine-error` for structurally different reasons and need different recovery:
|
|
1166
|
+
|
|
1167
|
+
**2a. Agent-spawn-dispatched issue (`AGENT_ISSUE_MAP[{NUMBER}]` set)** — unchanged from prior behavior. Resume the same agent in place:
|
|
1090
1168
|
```
|
|
1091
1169
|
Agent(
|
|
1092
1170
|
resume=AGENT_ISSUE_MAP[{NUMBER}],
|
|
@@ -1095,7 +1173,109 @@ done
|
|
|
1095
1173
|
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."
|
|
1096
1174
|
)
|
|
1097
1175
|
```
|
|
1098
|
-
|
|
1176
|
+
|
|
1177
|
+
**2b. Engine-first-dispatched issue (`ENGINE_DISPATCH_MAP[{NUMBER}]` set, no `AGENT_ISSUE_MAP` entry)** — `Agent(resume=...)` has nothing to resume here; the completed task was a backgrounded `Bash(command="forgedock run-issue ...")` call, not an `Agent()`. Re-issuing the identical `forgedock run-issue` command would just reproduce the same environmental failure. Instead, fall back to the Agent-spawn path for this ONE issue — but only when it is safe to do so:
|
|
1178
|
+
```bash
|
|
1179
|
+
# Safety gate: only fall back when the engine run committed NOTHING — no branch, no PR, no
|
|
1180
|
+
# committed phases. Primary source: the `FORGE:STATE` HTML-comment block the engine already
|
|
1181
|
+
# mirrors onto the issue BODY on every phase transition (`bin/engine/state.mjs`/`projector.mjs`
|
|
1182
|
+
# — see phase-3-dependency.md "Engine mode (default)"), which carries `committed`/`branch`/`pr`
|
|
1183
|
+
# as real JSON — read it directly rather than screen-scraping terminal text:
|
|
1184
|
+
# NOTE (forge#2750 review fix): `bin/engine/state.mjs`'s `serializeState()` writes the block
|
|
1185
|
+
# as THREE separate lines — `<!-- FORGE:STATE`, the JSON payload, and `-->` — never all on one
|
|
1186
|
+
# line. A single-line `grep -oP '(?<=<!-- FORGE:STATE)[\s\S]*?(?=-->)'` NEVER matches this <!-- allowlist:portability — cited non-portable anti-pattern being replaced, not a command this spec runs -->
|
|
1187
|
+
# (GNU `grep -P` without `-z` operates per-line, not across newlines, regardless of `[\s\S]`)
|
|
1188
|
+
# — it always returns empty. Extract the block with a `sed` line-range instead, which handles
|
|
1189
|
+
# the real multi-line shape correctly:
|
|
1190
|
+
STATE_BLOCK=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
|
|
1191
|
+
| sed -n '/<!-- FORGE:STATE/,/-->/p')
|
|
1192
|
+
STATE_JSON=$(echo "$STATE_BLOCK" | sed '1d;$d')
|
|
1193
|
+
if [ -n "$STATE_JSON" ]; then
|
|
1194
|
+
# Missing or schema-invalid fields are unverifiable, not empty. In particular, jq's
|
|
1195
|
+
# `length` is also zero for null, {}, and "", so require the committed array type first.
|
|
1196
|
+
# The persisted engine run ID and state version are also required: together they give every
|
|
1197
|
+
# orchestrator handling this same failure one stable scope while allowing a later failure
|
|
1198
|
+
# for the same issue/run name to elect a new claimant.
|
|
1199
|
+
if echo "$STATE_JSON" | jq -e \
|
|
1200
|
+
'(.run | type == "string" and test("^[A-Za-z0-9._:/-]+$")) and (.v | type == "number" and . >= 0 and floor == .) and (.committed | type == "array") and (.committed | length == 0) and (.branch == null) and (.pr == null)' \
|
|
1201
|
+
>/dev/null 2>&1; then
|
|
1202
|
+
STATE_RUN=$(echo "$STATE_JSON" | jq -r '.run')
|
|
1203
|
+
STATE_VERSION=$(echo "$STATE_JSON" | jq -r '.v')
|
|
1204
|
+
EMPTY_COMMITTED_STATE="true"
|
|
1205
|
+
else
|
|
1206
|
+
EMPTY_COMMITTED_STATE="false"
|
|
1207
|
+
fi
|
|
1208
|
+
else
|
|
1209
|
+
# No FORGE:STATE block was ever written to the issue body (e.g. the run failed before
|
|
1210
|
+
# phase 1 committed anything). There is no reliable structured signal available in this
|
|
1211
|
+
# case — fail SAFE: do NOT auto-fallback on an unverifiable state. This surfaces via the
|
|
1212
|
+
# existing stall-detection alert / needs-human path instead, exactly as before this fix.
|
|
1213
|
+
# (An earlier draft of this fix attempted a second fallback source — a
|
|
1214
|
+
# `committed=[] branch=null pr=null` diagnostic line assumed to be present in the
|
|
1215
|
+
# completed background Bash call's captured stdout — but no code in this file or
|
|
1216
|
+
# phase-3-dependency.md actually captures/populates that stdout into a named variable,
|
|
1217
|
+
# so that fallback was dead code reading an unset variable. Removed rather than wired up:
|
|
1218
|
+
# the FORGE:STATE block above is the engine's own durable, already-documented mirror of
|
|
1219
|
+
# this exact data and should always be present by the time a phase transition occurs;
|
|
1220
|
+
# treating its absence as "cannot verify, don't risk it" is the correct fail-safe.)
|
|
1221
|
+
EMPTY_COMMITTED_STATE="false"
|
|
1222
|
+
fi
|
|
1223
|
+
# A non-empty committed/branch/pr means real per-issue work exists that a fresh Agent-spawn
|
|
1224
|
+
# dispatch could collide with or duplicate — that case is NOT auto-fallen-back; it surfaces
|
|
1225
|
+
# via the existing stall-detection alert / needs-human path instead, exactly as before this fix.
|
|
1226
|
+
|
|
1227
|
+
if [ "$EMPTY_COMMITTED_STATE" = "true" ]; then
|
|
1228
|
+
# Atomic-enough cross-session claim: post first, then elect the lowest server-assigned
|
|
1229
|
+
# comment ID for this batch. Concurrent claimants may both post, but exactly one can win.
|
|
1230
|
+
# Pagination is mandatory; any POST/list/parse failure fails closed and dispatches nothing.
|
|
1231
|
+
CLAIM_SCOPE="${STATE_RUN}:${STATE_VERSION}"
|
|
1232
|
+
CLAIM_TOKEN="${CLAIM_SCOPE}-{NUMBER}-$$-${RANDOM}"
|
|
1233
|
+
ENGINE_FALLBACK_BODY="<!-- FORGE:ENGINE_FALLBACK -->
|
|
1234
|
+
## Engine-First Dispatch Failed — Falling Back to Agent-Spawn
|
|
1235
|
+
|
|
1236
|
+
**Batch**: ${CLAIM_SCOPE}
|
|
1237
|
+
**Claim**: ${CLAIM_TOKEN}
|
|
1238
|
+
|
|
1239
|
+
\`forgedock run-issue\` ended at \`workflow:engine-error\` with no committed phases, branch, or PR
|
|
1240
|
+
(\`committed=[] branch=null pr=null\`) — this is an environmental/tool failure (forge#2261), not a
|
|
1241
|
+
per-issue content failure, and nothing was committed that a fresh dispatch could collide with.
|
|
1242
|
+
Auto-falling back to the Agent-spawn \`/work-on\` path for this issue. See forge#2743."
|
|
1243
|
+
CLAIM_ID=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --method POST \
|
|
1244
|
+
--raw-field body="$ENGINE_FALLBACK_BODY" --jq '.id' 2>/dev/null) || CLAIM_ID=""
|
|
1245
|
+
CLAIM_LIST_OK="true"
|
|
1246
|
+
CLAIM_IDS=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
1247
|
+
--jq ".[] | select(.body | split(\"\\n\") | (any(. == \"<!-- FORGE:ENGINE_FALLBACK -->\") and any(. == \" **Batch**: ${CLAIM_SCOPE}\"))) | .id" \
|
|
1248
|
+
2>/dev/null) || CLAIM_LIST_OK="false"
|
|
1249
|
+
WINNER_CLAIM_ID=$(printf '%s\n' "$CLAIM_IDS" | grep -E '^[0-9]+$' | sort -n | head -1)
|
|
1250
|
+
CLAIM_WON="false"
|
|
1251
|
+
[ "$CLAIM_LIST_OK" = "true" ] && [ -n "$CLAIM_ID" ] && \
|
|
1252
|
+
[ "$CLAIM_ID" = "$WINNER_CLAIM_ID" ] && CLAIM_WON="true"
|
|
1253
|
+
|
|
1254
|
+
# Reuse the SAME Agent-spawn template as Step 4A's "Agent-spawn path (fallback when forgedock
|
|
1255
|
+
# CLI unavailable)" section above (the `Agent(subagent_type="general-purpose", ...)` block
|
|
1256
|
+
# under "Copy this template. Fill in variables. Do not modify the structure:") verbatim —
|
|
1257
|
+
# HARD RULE 1 preserved. This is a fresh dispatch, not a resume: capture its returned agent
|
|
1258
|
+
# ID into AGENT_ISSUE_MAP so subsequent completions for this issue route through 2a above.
|
|
1259
|
+
if [ "$CLAIM_WON" = "true" ]; then
|
|
1260
|
+
Agent(
|
|
1261
|
+
subagent_type="general-purpose",
|
|
1262
|
+
model="{SUBAGENT_MODEL}",
|
|
1263
|
+
description="Work on {PROJECT_PREFIX}#{NUMBER} (engine-error fallback)",
|
|
1264
|
+
run_in_background=true,
|
|
1265
|
+
prompt="<same template body as Step 4A's Agent-spawn path — see the 'Agent-spawn path (fallback when forgedock CLI unavailable)' section above; fill {NUMBER}/{GH_REPO}/{REPO_PATH}/{LANE}/{PR_BASE}/{ISSUE_TITLE} exactly as that template does>"
|
|
1266
|
+
)
|
|
1267
|
+
AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by the Agent() call above>
|
|
1268
|
+
# Re-occupies the worker slot this completion just freed (see "Concurrency slot release" above).
|
|
1269
|
+
ACTIVE_DISPATCH_COUNT=$((ACTIVE_DISPATCH_COUNT + 1))
|
|
1270
|
+
else
|
|
1271
|
+
echo "#{NUMBER}: engine fallback claim lost or could not be verified — no duplicate Agent dispatch."
|
|
1272
|
+
fi
|
|
1273
|
+
else
|
|
1274
|
+
echo "#{NUMBER}: engine-error with non-empty committed state (partial work exists) — NOT auto-falling back. Surfaces via standard stall-detection alert; forgedock resume-stalled remains available for manual/scripted recovery."
|
|
1275
|
+
fi
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
**Resume ALL stalled agents in a single message** (parallel resume/fallback across 2a and 2b together). Do not wait between resumes. Each resume or fallback dispatch re-occupies a worker slot — increment `ACTIVE_DISPATCH_COUNT` by 1 per agent resumed/dispatched here (it was already decremented by the "Concurrency slot release" rule above when the stall was first observed as a completion). <!-- Added: forge#1912 -->
|
|
1099
1279
|
|
|
1100
1280
|
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.
|
|
1101
1281
|
|
|
@@ -1391,17 +1571,9 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
|
|
|
1391
1571
|
RERESOLVE_ENABLED=$(yq '.orchestration.reresolve.enabled // true' forge.yaml 2>/dev/null || echo "true")
|
|
1392
1572
|
RERESOLVE_MAX_ROUNDS=$(yq '.orchestration.reresolve.max_rounds // "unbounded"' forge.yaml 2>/dev/null || echo "unbounded")
|
|
1393
1573
|
|
|
1394
|
-
node -
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const config = {
|
|
1398
|
-
enabled: process.argv[3] === "false" ? false : process.argv[3],
|
|
1399
|
-
maxRounds: process.argv[4] === "unbounded" ? undefined : Number(process.argv[4]),
|
|
1400
|
-
};
|
|
1401
|
-
const rounds = Number(process.argv[5]);
|
|
1402
|
-
console.log(JSON.stringify(shouldReResolve(classified, config, rounds)));
|
|
1403
|
-
}, () => process.exit(0));
|
|
1404
|
-
' "$ORIGINATING_QUERY_KIND" "$ORIGINATING_QUERY_PATTERN" "$RERESOLVE_ENABLED" "$RERESOLVE_MAX_ROUNDS" "$RERESOLVE_ROUNDS_SO_FAR"
|
|
1574
|
+
node "{REPO_PATH}/bin/engine/orchestrate-canary.mjs" \
|
|
1575
|
+
"$ORIGINATING_QUERY_KIND" "$ORIGINATING_QUERY_PATTERN" \
|
|
1576
|
+
"$RERESOLVE_ENABLED" "$RERESOLVE_MAX_ROUNDS" "$RERESOLVE_ROUNDS_SO_FAR"
|
|
1405
1577
|
```
|
|
1406
1578
|
|
|
1407
1579
|
If the result's `reResolve` is `false` (off switch, or `RERESOLVE_ROUNDS_SO_FAR` has reached `max_rounds` — bounded termination per the issue's acceptance criteria), skip this step for the current cycle and log the reason. `RERESOLVE_ROUNDS_SO_FAR` starts at 0 for the batch and increments once per cycle this step actually runs (declare it alongside the other Step 4A.pre batch-scope accumulators — do not re-initialize per completion).
|
|
@@ -1995,7 +2167,7 @@ Cascade-dispatched findings inherit NO lane assumption from the parent batch. St
|
|
|
1995
2167
|
|
|
1996
2168
|
```bash
|
|
1997
2169
|
for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
|
|
1998
|
-
PR_BASE=$(bash
|
|
2170
|
+
PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$FINDING_NUM" -R {GH_REPO}) || {
|
|
1999
2171
|
echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human label and removing from QUEUED_FINDINGS" >&2
|
|
2000
2172
|
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
2001
2173
|
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
@@ -2345,7 +2517,7 @@ declare -A SWEEP_LANE
|
|
|
2345
2517
|
declare -A SWEEP_PR_BASE
|
|
2346
2518
|
|
|
2347
2519
|
for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
|
|
2348
|
-
SWEEP_BASE=$(bash
|
|
2520
|
+
SWEEP_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$FINDING_NUM" -R {GH_REPO}) || {
|
|
2349
2521
|
echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human and skipping" >&2
|
|
2350
2522
|
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
2351
2523
|
gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
@@ -2526,4 +2698,3 @@ fi
|
|
|
2526
2698
|
```
|
|
2527
2699
|
|
|
2528
2700
|
---
|
|
2529
|
-
|
|
@@ -125,7 +125,16 @@ Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85
|
|
|
125
125
|
### B1C: Create worktree
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
-
|
|
128
|
+
WORKTREE_ROOT="/path/to/repo/.claude/worktrees"
|
|
129
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
130
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
131
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
132
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
133
|
+
WORKTREE_ROOT="/path/to/repo/.opencode/worktrees"
|
|
134
|
+
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
135
|
+
WORKTREE_ROOT="/path/to/repo/.codex/worktrees"
|
|
136
|
+
fi
|
|
137
|
+
WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
|
|
129
138
|
git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
|
|
130
139
|
```
|
|
131
140
|
|
package/commands/work-on.md
CHANGED
|
@@ -1411,7 +1411,16 @@ esac
|
|
|
1411
1411
|
cd {REPO_PATH}
|
|
1412
1412
|
git fetch origin
|
|
1413
1413
|
BRANCH="fix/{slug}-{NUMBER}"
|
|
1414
|
-
|
|
1414
|
+
WORKTREE_ROOT="{REPO_PATH}/.claude/worktrees"
|
|
1415
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
1416
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
1417
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
1418
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
1419
|
+
WORKTREE_ROOT="{REPO_PATH}/.opencode/worktrees"
|
|
1420
|
+
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
1421
|
+
WORKTREE_ROOT="{REPO_PATH}/.codex/worktrees"
|
|
1422
|
+
fi
|
|
1423
|
+
WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
|
|
1415
1424
|
git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
|
|
1416
1425
|
```
|
|
1417
1426
|
|