forgedock 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- # Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
356
- # (installed by `npx forgedock` — see bin/journey.mjs: forge()'s linkPipelineScripts() step)
357
- # NOTE: the actual invocations below (and in the review-finding sweep) hardcode this
358
- # ~/.claude path directly — there is no $FORGE_HOME-based fallback implemented, so this
359
- # call site is not exposed to the bare-unset-$FORGE_HOME → root-anchored-path footgun
360
- # that affected commands/review-pr.md (see forge#1984, forge#2035 audit). If
361
- # ~/.claude/scripts/ is genuinely unavailable, classify-lane.sh hard-fails per the
362
- # phantom-slug gate above — it does NOT fall through to a filesystem search.
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
@@ -391,6 +416,9 @@ declare -A AGENT_ISSUE_MAP
391
416
  # this map to identify which issue a background-Bash completion notification belongs to,
392
417
  # the same role AGENT_ISSUE_MAP plays for Agent-tool `agent_completed` notifications.
393
418
  declare -A ENGINE_DISPATCH_MAP
419
+ # OpenCode native task equivalent of ENGINE_DISPATCH_MAP. Keys are issue numbers;
420
+ # values are the session ids returned by task(background=true).
421
+ declare -A OPENCODE_DISPATCH_MAP
394
422
 
395
423
  # Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
396
424
  # dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
@@ -498,7 +526,7 @@ SURFACE_BATCHED_FINDINGS=() # all member issue numbers absorbed into a batch a
498
526
  SURFACE_BATCH_COUNT=0 # count of batch issues created across the run
499
527
 
500
528
  for NUM in {ready_issue_numbers}; do
501
- PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
529
+ PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$NUM" -R {GH_REPO}) || {
502
530
  echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
503
531
  gh issue edit "$NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
504
532
  continue
@@ -521,9 +549,64 @@ Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and
521
549
 
522
550
  **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
551
 
552
+ **OpenCode dispatch (runtime-neutral adapter)**: When `FORGE_RUNTIME=opencode` or
553
+ an OpenCode runtime marker is present, do not invoke the engine's Claude-backed
554
+ runner and do not use the Claude `Agent(...)` fallback. Dispatch each ready
555
+ issue through OpenCode's native `task` tool, loading the nested
556
+ `commands/work-on.md` pipeline. Use GitHub labels and `FORGE:*` comments as the
557
+ resume state. After each task continuation, re-read the issue workflow label
558
+ and continue until it reaches
559
+ `workflow:merged`, `workflow:invalid`, `needs-human`, or
560
+ `workflow:awaiting-merge`.
561
+
562
+ The native call is asynchronous and must be issued once per issue in
563
+ `DISPATCH_NOW`, in the same assistant message where the batch is dispatched:
564
+
565
+ ```
566
+ task(
567
+ description="Work on {PROJECT_PREFIX}#{NUMBER}",
568
+ subagent_type="general",
569
+ background=true,
570
+ prompt="Use the same Phase 4A work-on template below. Invoke Skill(skill='work-on', args='{PROJECT_PREFIX}{NUMBER} --under-orchestration') and continue until a terminal workflow state."
571
+ )
572
+ ```
573
+
574
+ Capture the returned `<task id="..." state="running">` id in
575
+ `OPENCODE_DISPATCH_MAP[{NUMBER}]`. `background=true` is required for streaming
576
+ DAG behavior; a foreground task makes the orchestrator wait for that issue and
577
+ reintroduces a wave barrier. The ForgeDock OpenCode plugin opts into
578
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` by default. An explicit
579
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is a supported degraded mode,
580
+ but it must be reported as non-streaming rather than treated as equivalent to
581
+ Claude background dispatch. Do not wait for the slowest sibling before handling
582
+ an earlier task-result event.
583
+
584
+ ```bash
585
+ if [ "${FORGE_RUNTIME:-}" = "opencode" ]; then
586
+ echo "OpenCode native task dispatch selected — load commands/work-on.md for each ready issue."
587
+ echo "Do not call forgedock run-issue or the Claude Agent(...) fallback in this branch."
588
+ echo "Use task(subagent_type=general, background=true) once per ready issue and process each task-result event immediately."
589
+ echo "If native task dispatch cannot run, post FORGE:OPENCODE_BLOCKED with the missing capability and add needs-human."
590
+ fi
591
+ ```
592
+
593
+ This branch is additive. When OpenCode is not selected, continue through the
594
+ existing engine-first and Claude Agent-spawn paths below without modification.
595
+
596
+ The generated OpenCode plugin is an executable boundary, not just prompt
597
+ guidance: its `tool.execute.before` hook rejects `claude`, `forgedock
598
+ run-issue`, `npx forgedock run-issue`, and recursive `opencode run` shell
599
+ commands with `FORGE_OPENCODE_CAPABILITY_ERROR`. If a native Skill or Task is
600
+ unavailable, stop with that bounded error and preserve the GitHub label state;
601
+ never compensate by starting a second controller or the Claude-backed engine.
602
+
524
603
  **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
604
 
526
605
  ```bash
606
+ if [ "${FORGE_RUNTIME:-}" != "opencode" ] &&
607
+ [ -z "${OPENCODE_SESSION_ID:-}" ] &&
608
+ [ -z "${OPENCODE_PID:-}" ] &&
609
+ [ -z "${OPENCODE:-}" ]; then
527
610
  # Engine-first dispatch: check CLI availability, then dispatch ready issues in score order
528
611
  # Uses SORTED_READY_SET[] from Step 3E.5 (descending value/cost) and budget gate from Step 4A-pre.0
529
612
  FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || echo "false")
@@ -536,20 +619,13 @@ FORGEDOCK_AVAILABLE=$(command -v forgedock >/dev/null 2>&1 && echo "true" || ech
536
619
  # `forgedock run-issue` call fails within seconds, and nothing re-routes the batch — a human has
537
620
  # to notice and manually re-dispatch via the Agent-spawn path below.
538
621
  #
539
- # CRITICAL — do NOT use a shell-resolved probe here (`command -v claude`, `claude --version` run
540
- # directly by bash, `which claude`, etc.). forge#2741's entire root cause is that a shell-resolved
541
- # probe and the engine's actual invocation resolve the `claude` binary differently: bash's builtin
542
- # PATH lookup (and `execSync("claude --version", ...)`, used deliberately by
543
- # `bin/runner.mjs`'s `isClaudeCliAvailable()` for this exact reason) can find and run an
544
- # npx-transient/extensionless shim that `bin/runner.mjs`'s actual backend call —
545
- # `runCliBackend()`'s `spawnFn(bin, cliArgs, { shell: false, ... })` with bare `bin = "claude"`
546
- # (no shell resolution) — cannot. A canary that shell-resolves `claude` reproduces the exact
547
- # blind spot it exists to catch and reports the backend healthy in precisely forge#2741's failure
548
- # scenario. The canary MUST reproduce `runCliBackend()`'s exact invocation shape — bare binary
549
- # name, `shell: false` — via a one-off Node `spawnSync` call, not a bash-native probe:
622
+ # Use the runner's tested backend policy rather than duplicating it as shell/Node prose. This
623
+ # reuses the production resolved-CLI path check (including transient-shim refresh), honors an
624
+ # explicit FORGEDOCK_BACKEND, and accepts API fallback only when ANTHROPIC_API_KEY is configured.
625
+ # It performs no paid model request.
550
626
  if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
551
- if ! node -e "const r = require('node:child_process').spawnSync('claude', ['--version'], { shell: false }); process.exit(r.error ? 1 : 0);" 2>/dev/null; then
552
- echo "WARNING: forgedock CLI is on PATH, but its execution backend is not invocable via the engine's own spawn shape (bare 'claude', shell:false) in this environment. Downgrading to the Agent-spawn fallback path for this entire run — see forge#2743. (If forge#2741's ENOENT root cause is fixed, this canary will pass again on the next /orchestrate invocation.)"
627
+ if ! forgedock backend-check --quiet; then
628
+ 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."
553
629
  FORGEDOCK_AVAILABLE="false"
554
630
  fi
555
631
  fi
@@ -624,6 +700,7 @@ else
624
700
  # Fall through to Agent-spawn template below. The SubagentStop hook (bin/hooks/interactive-engine.mjs)
625
701
  # bridges these runs to the engine run-log for state persistence even on the fallback path.
626
702
  fi
703
+ fi
627
704
  ```
628
705
 
629
706
  **Dispatch each issue in `DISPATCH_NOW` via its own backgrounded `Bash` call (MANDATORY when `FORGEDOCK_AVAILABLE=true`) — never shell `&`/`wait`.** Issue one `Bash(...)` call per issue in `DISPATCH_NOW`, all in the same message, so they run concurrently within the headroom already computed above:
@@ -643,7 +720,13 @@ ENGINE_DISPATCH_MAP[{NUM}] = <task_id returned by Bash(run_in_background=true, .
643
720
 
644
721
  If `DISPATCH_NOW` is empty (headroom is 0), do not dispatch any issues this cycle — wait for the next completion notification, exactly like the Agent-spawn path.
645
722
 
646
- **Agent-spawn path (fallback when forgedock CLI unavailable)**: When `FORGEDOCK_AVAILABLE=false`, spawn Agent sub-agents per issue using the template below. This preserves engine state via the SubagentStop hook even without the CLI.
723
+ **Agent-spawn path (Claude fallback when forgedock CLI unavailable)**: When `FORGEDOCK_AVAILABLE=false` and the runtime is not OpenCode, spawn Agent sub-agents per issue using the template below. This preserves engine state via the SubagentStop hook even without the CLI.
724
+
725
+ When the runtime is OpenCode, skip this Claude-only `Agent(...)` path entirely.
726
+ Use the native `task` call above for initial, newly-ready, remediation, and
727
+ stall-resume dispatches. Translate every later `Agent(...)` reference in this
728
+ phase to `task(...)` with `subagent_type="general"`; preserve `background=true`
729
+ and use `task_id=OPENCODE_DISPATCH_MAP[{NUMBER}]` when resuming a child.
647
730
 
648
731
  **REMINDER: You MUST use the template below verbatim when on the Agent-spawn fallback path. Only fill in `{VARIABLES}`. Do NOT rewrite the agent prompt. Do NOT write custom implementation instructions. The agent MUST invoke `/work-on` via the Skill tool — this is the HARD RULE from the top of this file.**
649
732
 
@@ -873,9 +956,9 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle
873
956
 
874
957
  ### Step 4B: Monitor completions and dispatch newly ready issues
875
958
 
876
- 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).
959
+ You will be automatically notified when each background agent completes — or, for an engine-first dispatch (Step 4A, `FORGEDOCK_AVAILABLE=true`), when a backgrounded `Bash(run_in_background=true, command="forgedock run-issue ...")` call completes. In OpenCode, a native background `task` first returns a `state="running"` result and later injects a synthetic `<task id="..." state="completed">` or `state="error"` result into this same parent session. The later result is the completion event; the initial running result is not completion. Both Claude notifications and OpenCode task-result events must trigger the same per-issue handling immediately. **Do NOT use `sleep` loops, wait for the slowest sibling, or poll before processing an event.** When one arrives, look up which issue it belongs to — `AGENT_ISSUE_MAP` for a Claude `agent_completed` notification, `ENGINE_DISPATCH_MAP` for a background-Bash notification, or `OPENCODE_DISPATCH_MAP` for an OpenCode task result — and immediately process it exactly the same way regardless of which map resolved it; every check below (`classify_predecessor_state()`, dependent dispatch, stall/staging checks) keys off GitHub labels/state, not which dispatch mechanism produced them. **Fallback**: if a runtime ever fails to deliver a background completion notification, use the documented label-state recovery path, but do not convert the normal OpenCode path to foreground tasks or a wave barrier.
877
960
 
878
- **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.)
961
+ **Concurrency slot release (MANDATORY — first action on every completion)** <!-- Added: forge#1912 -->: The instant a completion notification arrives — `agent_completed`, a background-Bash completion, or an OpenCode `task` result with `state="completed"`/`state="error"` — decrement `ACTIVE_DISPATCH_COUNT` by 1 — a worker slot has just freed, regardless of what terminal state the issue ended up in. Do this before any stall-recovery/resume logic below. If that agent is then resumed or fallback-dispatched (item 2 below, or Step 4B.5's stall recovery) because it stalled mid-pipeline rather than truly finishing, re-increment `ACTIVE_DISPATCH_COUNT` when the `Agent(resume=...)` or OpenCode `task(task_id=..., background=true)` 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 resume-specific and out of scope for this fix: an engine-dispatched issue that silently hangs still surfaces only via the standard stall-detection alert, and `forgedock resume-stalled` remains available as a separate manual/scripted recovery path for that case. The completion-triggered case — an engine-dispatched issue that DOES complete at `workflow:engine-error` with empty committed state — is still auto-fallen-back by item 2b below, fixed forge#2743.)
879
962
 
880
963
  **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.
881
964
 
@@ -1132,10 +1215,6 @@ done
1132
1215
 
1133
1216
  **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:
1134
1217
  ```bash
1135
- # Idempotency guard: only fall back once per issue per engine-error occurrence.
1136
- ALREADY_FALLEN_BACK=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
1137
- --jq '[.[] | select(.body | contains("FORGE:ENGINE_FALLBACK"))] | length' 2>/dev/null || echo "0")
1138
-
1139
1218
  # Safety gate: only fall back when the engine run committed NOTHING — no branch, no PR, no
1140
1219
  # committed phases. Primary source: the `FORGE:STATE` HTML-comment block the engine already
1141
1220
  # mirrors onto the issue BODY on every phase transition (`bin/engine/state.mjs`/`projector.mjs`
@@ -1149,13 +1228,22 @@ done
1149
1228
  # the real multi-line shape correctly:
1150
1229
  STATE_BLOCK=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
1151
1230
  | sed -n '/<!-- FORGE:STATE/,/-->/p')
1152
- STATE_JSON=$(echo "$STATE_BLOCK" | sed '1d;$d')
1153
- if [ -n "$STATE_JSON" ]; then
1154
- COMMITTED_COUNT=$(echo "$STATE_JSON" | jq -r '.committed | length' 2>/dev/null)
1155
- STATE_BRANCH=$(echo "$STATE_JSON" | jq -r '.branch' 2>/dev/null)
1156
- STATE_PR=$(echo "$STATE_JSON" | jq -r '.pr' 2>/dev/null)
1157
- [ "$COMMITTED_COUNT" = "0" ] && [ "$STATE_BRANCH" = "null" ] && [ "$STATE_PR" = "null" ] \
1158
- && EMPTY_COMMITTED_STATE="true" || EMPTY_COMMITTED_STATE="false"
1231
+ STATE_JSON=$(echo "$STATE_BLOCK" | sed '1d;$d')
1232
+ if [ -n "$STATE_JSON" ]; then
1233
+ # Missing or schema-invalid fields are unverifiable, not empty. In particular, jq's
1234
+ # `length` is also zero for null, {}, and "", so require the committed array type first.
1235
+ # The persisted engine run ID and state version are also required: together they give every
1236
+ # orchestrator handling this same failure one stable scope while allowing a later failure
1237
+ # for the same issue/run name to elect a new claimant.
1238
+ if echo "$STATE_JSON" | jq -e \
1239
+ '(.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)' \
1240
+ >/dev/null 2>&1; then
1241
+ STATE_RUN=$(echo "$STATE_JSON" | jq -r '.run')
1242
+ STATE_VERSION=$(echo "$STATE_JSON" | jq -r '.v')
1243
+ EMPTY_COMMITTED_STATE="true"
1244
+ else
1245
+ EMPTY_COMMITTED_STATE="false"
1246
+ fi
1159
1247
  else
1160
1248
  # No FORGE:STATE block was ever written to the issue body (e.g. the run failed before
1161
1249
  # phase 1 committed anything). There is no reliable structured signal available in this
@@ -1175,32 +1263,53 @@ done
1175
1263
  # dispatch could collide with or duplicate — that case is NOT auto-fallen-back; it surfaces
1176
1264
  # via the existing stall-detection alert / needs-human path instead, exactly as before this fix.
1177
1265
 
1178
- if [ "$ALREADY_FALLEN_BACK" -eq 0 ] && [ "$EMPTY_COMMITTED_STATE" = "true" ]; then
1266
+ if [ "$EMPTY_COMMITTED_STATE" = "true" ]; then
1267
+ # Atomic-enough cross-session claim: post first, then elect the lowest server-assigned
1268
+ # comment ID for this batch. Concurrent claimants may both post, but exactly one can win.
1269
+ # Pagination is mandatory; any POST/list/parse failure fails closed and dispatches nothing.
1270
+ CLAIM_SCOPE="${STATE_RUN}:${STATE_VERSION}"
1271
+ CLAIM_TOKEN="${CLAIM_SCOPE}-{NUMBER}-$$-${RANDOM}"
1179
1272
  ENGINE_FALLBACK_BODY="<!-- FORGE:ENGINE_FALLBACK -->
1180
1273
  ## Engine-First Dispatch Failed — Falling Back to Agent-Spawn
1181
1274
 
1275
+ **Batch**: ${CLAIM_SCOPE}
1276
+ **Claim**: ${CLAIM_TOKEN}
1277
+
1182
1278
  \`forgedock run-issue\` ended at \`workflow:engine-error\` with no committed phases, branch, or PR
1183
1279
  (\`committed=[] branch=null pr=null\`) — this is an environmental/tool failure (forge#2261), not a
1184
1280
  per-issue content failure, and nothing was committed that a fresh dispatch could collide with.
1185
1281
  Auto-falling back to the Agent-spawn \`/work-on\` path for this issue. See forge#2743."
1186
- gh issue comment {NUMBER} {GH_FLAG} --body "$ENGINE_FALLBACK_BODY" # <!-- allowlist:check-command-side-effects — intentional pipeline status annotation, mirrors work-on/build/architect.md:700 -->
1282
+ CLAIM_ID=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --method POST \
1283
+ --raw-field body="$ENGINE_FALLBACK_BODY" --jq '.id' 2>/dev/null) || CLAIM_ID=""
1284
+ CLAIM_LIST_OK="true"
1285
+ CLAIM_IDS=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
1286
+ --jq ".[] | select(.body | split(\"\\n\") | (any(. == \"<!-- FORGE:ENGINE_FALLBACK -->\") and any(. == \" **Batch**: ${CLAIM_SCOPE}\"))) | .id" \
1287
+ 2>/dev/null) || CLAIM_LIST_OK="false"
1288
+ WINNER_CLAIM_ID=$(printf '%s\n' "$CLAIM_IDS" | grep -E '^[0-9]+$' | sort -n | head -1)
1289
+ CLAIM_WON="false"
1290
+ [ "$CLAIM_LIST_OK" = "true" ] && [ -n "$CLAIM_ID" ] && \
1291
+ [ "$CLAIM_ID" = "$WINNER_CLAIM_ID" ] && CLAIM_WON="true"
1187
1292
 
1188
1293
  # Reuse the SAME Agent-spawn template as Step 4A's "Agent-spawn path (fallback when forgedock
1189
1294
  # CLI unavailable)" section above (the `Agent(subagent_type="general-purpose", ...)` block
1190
1295
  # under "Copy this template. Fill in variables. Do not modify the structure:") verbatim —
1191
1296
  # HARD RULE 1 preserved. This is a fresh dispatch, not a resume: capture its returned agent
1192
1297
  # ID into AGENT_ISSUE_MAP so subsequent completions for this issue route through 2a above.
1193
- Agent(
1298
+ if [ "$CLAIM_WON" = "true" ]; then
1299
+ Agent(
1194
1300
  subagent_type="general-purpose",
1195
1301
  model="{SUBAGENT_MODEL}",
1196
1302
  description="Work on {PROJECT_PREFIX}#{NUMBER} (engine-error fallback)",
1197
1303
  run_in_background=true,
1198
1304
  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>"
1199
- )
1200
- AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by the Agent() call above>
1201
- # Re-occupies the worker slot this completion just freed (see "Concurrency slot release" above).
1202
- ACTIVE_DISPATCH_COUNT=$((ACTIVE_DISPATCH_COUNT + 1))
1203
- elif [ "$ALREADY_FALLEN_BACK" -eq 0 ]; then
1305
+ )
1306
+ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by the Agent() call above>
1307
+ # Re-occupies the worker slot this completion just freed (see "Concurrency slot release" above).
1308
+ ACTIVE_DISPATCH_COUNT=$((ACTIVE_DISPATCH_COUNT + 1))
1309
+ else
1310
+ echo "#{NUMBER}: engine fallback claim lost or could not be verified — no duplicate Agent dispatch."
1311
+ fi
1312
+ else
1204
1313
  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."
1205
1314
  fi
1206
1315
  ```
@@ -1501,17 +1610,9 @@ Gating predecessor #${NUM} reached \`workflow:merged\` — dispatching now. (Was
1501
1610
  RERESOLVE_ENABLED=$(yq '.orchestration.reresolve.enabled // true' forge.yaml 2>/dev/null || echo "true")
1502
1611
  RERESOLVE_MAX_ROUNDS=$(yq '.orchestration.reresolve.max_rounds // "unbounded"' forge.yaml 2>/dev/null || echo "unbounded")
1503
1612
 
1504
- node -e '
1505
- import("{REPO_PATH}/bin/engine/resolve.mjs").then(({ shouldReResolve }) => {
1506
- const classified = { kind: process.argv[1], pattern: process.argv[2], args: [] };
1507
- const config = {
1508
- enabled: process.argv[3] === "false" ? false : process.argv[3],
1509
- maxRounds: process.argv[4] === "unbounded" ? undefined : Number(process.argv[4]),
1510
- };
1511
- const rounds = Number(process.argv[5]);
1512
- console.log(JSON.stringify(shouldReResolve(classified, config, rounds)));
1513
- }, () => process.exit(0));
1514
- ' "$ORIGINATING_QUERY_KIND" "$ORIGINATING_QUERY_PATTERN" "$RERESOLVE_ENABLED" "$RERESOLVE_MAX_ROUNDS" "$RERESOLVE_ROUNDS_SO_FAR"
1613
+ node "{REPO_PATH}/bin/engine/orchestrate-canary.mjs" \
1614
+ "$ORIGINATING_QUERY_KIND" "$ORIGINATING_QUERY_PATTERN" \
1615
+ "$RERESOLVE_ENABLED" "$RERESOLVE_MAX_ROUNDS" "$RERESOLVE_ROUNDS_SO_FAR"
1515
1616
  ```
1516
1617
 
1517
1618
  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).
@@ -2105,7 +2206,7 @@ Cascade-dispatched findings inherit NO lane assumption from the parent batch. St
2105
2206
 
2106
2207
  ```bash
2107
2208
  for FINDING_NUM in "${QUEUED_FINDINGS[@]}"; do
2108
- PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$FINDING_NUM" -R {GH_REPO}) || {
2209
+ PR_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$FINDING_NUM" -R {GH_REPO}) || {
2109
2210
  echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human label and removing from QUEUED_FINDINGS" >&2
2110
2211
  # 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
2111
2212
  gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
@@ -2455,7 +2556,7 @@ declare -A SWEEP_LANE
2455
2556
  declare -A SWEEP_PR_BASE
2456
2557
 
2457
2558
  for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
2458
- SWEEP_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$FINDING_NUM" -R {GH_REPO}) || {
2559
+ SWEEP_BASE=$(bash "$CLASSIFY_LANE_SCRIPT" "$FINDING_NUM" -R {GH_REPO}) || {
2459
2560
  echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human and skipping" >&2
2460
2561
  # 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
2461
2562
  gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
@@ -2636,4 +2737,3 @@ fi
2636
2737
  ```
2637
2738
 
2638
2739
  ---
2639
-
@@ -19,6 +19,14 @@ the `prompt` argument to whichever tool the orchestrator resolved (`Task(...)` o
19
19
  same per-agent isolation and the same requirement that each agent posts structured findings to the PR
20
20
  directly (`gh pr comment`) rather than relaying them through the orchestrator's own context.
21
21
 
22
+ **OpenCode override**: When `FORGE_RUNTIME=opencode` or an equivalent OpenCode marker is present, the
23
+ orchestrator must set `DISPATCH_TOOL=task` before checking literal Claude tool names. A native task
24
+ argument is shaped as `{ description: "...", prompt: "...", subagent_type: "general"|"explore", background: true }`:
25
+ use `general` for implementation/review and `explore` for read-only discovery. The `Neither tool is
26
+ available` hard stop applies only when lowercase native `task` is genuinely absent; it must not be
27
+ triggered by the absence of `Task` and `Agent` names in an OpenCode session. If native `task` is absent,
28
+ post `FORGE:REVIEW_BLOCKED` and never replace the isolated review with inline work.
29
+
22
30
  <!-- FORGE:PROTOCOL_SOURCE — canonical definition lives in docs/spec/review-protocol.md -->
23
31
 
24
32
  ---
@@ -23,6 +23,24 @@ Performs comprehensive review of `staging` before merging to `main`. Handles lar
23
23
 
24
24
  This spec dispatches Bug Hunter and domain review agents via a sub-agent-spawning tool. Different runtimes expose different tools for this — resolve deterministically, once per invocation, and use the same tool for every dispatch call in this run:
25
25
 
26
+ ### OpenCode Runtime Override
27
+
28
+ When the workflow is running under OpenCode, resolve the native tool before applying the Claude-specific availability order above:
29
+
30
+ ```bash
31
+ IS_OPENCODE_RUNTIME=false
32
+ if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
33
+ [ -n "${OPENCODE_SESSION_ID:-}" ] ||
34
+ [ -n "${OPENCODE_PID:-}" ] ||
35
+ [ -n "${OPENCODE:-}" ]; then
36
+ IS_OPENCODE_RUNTIME=true
37
+ DISPATCH_TOOL=task
38
+ DISPATCH_SUBAGENT_TYPE=general
39
+ fi
40
+ ```
41
+
42
+ When `IS_OPENCODE_RUNTIME=true`, lowercase native `task` is the preferred isolated dispatch tool. Do not enter the `Neither tool is available` branch merely because Claude's literal `Task` and `Agent` names are absent. Every native task call must use a top-level `subagent_type`: `general` for implementation/review work and `explore` for read-only discovery (Claude `general-purpose` and `codebase-explorer` map to those values). Use `background: true` for independent reviewers so the parent can process each completion event without a wave barrier. If the native `task` capability itself is absent from the current tool registry, use the existing hard-stop path and post `FORGE:REVIEW_BLOCKED`; never substitute inline review.
43
+
26
44
  1. **If `Task` is available in the current environment**: set `{DISPATCH_TOOL} = Task`. This is the preferred tool — tightest `allowed-tools` scoping. (Identical resolution logic to `/review-pr` Phase 3C — do not diverge.)
27
45
  2. **Else if `Agent` is available**: set `{DISPATCH_TOOL} = Agent`. This is the documented fallback, not a degraded path — use it exactly as you would `Task`: one call per selected agent (Bug Hunters in Phase 3, Code Quality in Phase 4, domain agents in Phase 5), same prompt template, `subagent_type: "general-purpose"` (or the closest equivalent the environment offers), same requirement that each agent posts its own findings directly to the PR via `gh pr comment`. Isolation and fresh-context review are preserved either way.
28
46
  3. **Neither tool is available**: this is a genuine setup defect, not a routing decision — HARD STOP, post a PR/issue comment explaining that no sub-agent dispatch tool is available, add `needs-human`, and exit without posting a verdict. Do NOT fall back to reviewing inline in the orchestrator's own context.
@@ -33,6 +33,24 @@ allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch, Skill
33
33
 
34
34
  This spec dispatches domain review agents via a sub-agent-spawning tool. Different runtimes expose different tools for this — resolve deterministically, once per invocation, and use the same tool for every dispatch call in this run:
35
35
 
36
+ ### OpenCode Runtime Override
37
+
38
+ When the workflow is running under OpenCode, resolve the native tool before applying the Claude-specific availability order above:
39
+
40
+ ```bash
41
+ IS_OPENCODE_RUNTIME=false
42
+ if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
43
+ [ -n "${OPENCODE_SESSION_ID:-}" ] ||
44
+ [ -n "${OPENCODE_PID:-}" ] ||
45
+ [ -n "${OPENCODE:-}" ]; then
46
+ IS_OPENCODE_RUNTIME=true
47
+ DISPATCH_TOOL=task
48
+ DISPATCH_SUBAGENT_TYPE=general
49
+ fi
50
+ ```
51
+
52
+ When `IS_OPENCODE_RUNTIME=true`, lowercase native `task` is the preferred isolated dispatch tool. Do not enter the `Neither tool is available` branch merely because Claude's literal `Task` and `Agent` names are absent. Every native task call must use a top-level `subagent_type`: `general` for implementation/review work and `explore` for read-only discovery (Claude `general-purpose` and `codebase-explorer` map to those values). Use `background: true` for independent reviewers so the parent can process each completion event without a wave barrier. If the native `task` capability itself is absent from the current tool registry, use the existing hard-stop path and post `FORGE:REVIEW_BLOCKED`; never substitute inline review.
53
+
36
54
  1. **If `Task` is available in the current environment**: set `{DISPATCH_TOOL} = Task`. This is the preferred tool — tightest `allowed-tools` scoping.
37
55
  2. **Else if `Agent` is available**: set `{DISPATCH_TOOL} = Agent`. This is the documented fallback, not a degraded path — use it exactly as you would `Task`: one call per selected domain agent, same prompt template, `subagent_type: "general-purpose"` (or the closest equivalent the environment offers), same requirement that each agent posts its own findings directly to the PR via `gh pr comment`. Isolation and fresh-context review are preserved either way.
38
56
  3. **Neither tool is available**: this is a genuine setup defect, not a routing decision — HARD STOP, post a PR/issue comment explaining that no sub-agent dispatch tool is available, add `needs-human`, and exit without posting a verdict. Do NOT fall back to reviewing inline in the orchestrator's own context — inline self-review is strictly weaker than an isolated fresh-context reviewer and is never a substitute for a missing dispatch tool.
@@ -1520,7 +1538,7 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
1520
1538
  6. Substitute code index slice: `[INDEX_SLICE]` → the matching `$INDEX_SLICE_{DOMAIN}` variable for this agent (e.g., `$INDEX_SLICE_AUTH` for the auth agent). Agents MUST query index data first; fall back to grep only when index slice is empty or unavailable.
1521
1539
  7. Substitute per-agent diff slice: `[DOMAIN_DIFF_SLICE]` → the matching `$DIFF_SLICE_*` variable (e.g., `$DIFF_SLICE_AUTH` for the auth agent, `$DIFF_SLICE_SECURITY` for the security agent). This replaces any `gh pr diff [PR_NUMBER]` call inside the agent template — the agent works from the pre-computed slice, not the full changeset.
1522
1540
  8. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
1523
- 9. Launch via the resolved `{DISPATCH_TOOL}` (see Sub-Agent Dispatch Tool Resolution above) with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited)
1541
+ 9. Launch via the resolved `{DISPATCH_TOOL}` (see Sub-Agent Dispatch Tool Resolution above) with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited). Under OpenCode, emit a top-level `subagent_type: "general"` or `"explore"` in the native `task` argument object and use `background: true` for each independent reviewer.
1524
1542
 
1525
1543
  **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. Each agent posts findings directly to the PR via `gh pr comment`.
1526
1544
 
@@ -1553,7 +1571,7 @@ gh pr view $ARGUMENTS --json comments --jq '.comments | length'
1553
1571
  gh api repos/{owner}/{repo}/issues/$ARGUMENTS/comments --jq '.[-10:] | .[].body[:100]'
1554
1572
  ```
1555
1573
 
1556
- **Do NOT proceed until ALL launched agent comments are visible on the PR.**
1574
+ **Do NOT proceed until ALL launched agent comments are visible on the PR.** Under OpenCode, the initial `task(..., background: true)` result with `state="running"` is not completion; wait for and process each injected `task` result event, then verify the corresponding structured PR comment before continuing.
1557
1575
 
1558
1576
  ---
1559
1577
 
@@ -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
- WORKTREE_PATH="/path/to/repo/.claude/worktrees/{BRANCH_SLUG}"
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
 
@@ -1411,7 +1411,16 @@ esac
1411
1411
  cd {REPO_PATH}
1412
1412
  git fetch origin
1413
1413
  BRANCH="fix/{slug}-{NUMBER}"
1414
- WORKTREE_PATH="{REPO_PATH}/.claude/worktrees/{BRANCH_SLUG}"
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
 
package/docs/CONFIG.md CHANGED
@@ -927,7 +927,7 @@ The hook entry is registered idempotently: re-installing (`npx forgedock update`
927
927
  | Command | Action |
928
928
  |---------|--------|
929
929
  | `npx forgedock` / `npx forgedock install` | Registers the SessionStart hook (along with installing commands and writing `forge.yaml`) |
930
- | `npx forgedock update` | Re-registers the hook (relink only — does not touch `forge.yaml`) |
930
+ | `npx forgedock update` | Re-registers the hook when the resolved source can be safely refreshed (relink only — does not touch `forge.yaml`) |
931
931
  | `npx forgedock uninstall` | Removes the hook entry from `settings.json` |
932
932
 
933
933
  ### Opting Out
@@ -948,7 +948,7 @@ See [Per-Directory State Registry](#per-directory-state-registry) below for how
948
948
 
949
949
  `npx`/`npm` never installs `forgedock` to a stable location on your machine — it resolves to wherever `npx`'s cache (or a global npm install, or a git clone) happens to physically live. Historically, `~/.claude/commands/` and the SessionStart hook's script path were symlinked **directly** from that ephemeral location. When npm/npx (or the OS) later evicted its cache, those symlinks silently dangled — Claude Code sessions stopped getting ForgeDock context with no error shown.
950
950
 
951
- As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
951
+ For npm/npx sources, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
952
952
 
953
953
  ```
954
954
  ~/.forge/
@@ -968,8 +968,8 @@ As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies
968
968
  | `npx forgedock` / `npx forgedock install` | Copies the resolved package's payload into `~/.forge/` (content-compared — unchanged files are never rewritten) before linking commands, then links from `~/.forge/`, not the original source |
969
969
  | `npx forgedock init` | Does not touch `~/.forge/` — `init` only writes `forge.yaml`, it never installs commands |
970
970
  | `npx forgedock update` (npm/npx install) | Refreshes `~/.forge/` from whatever the currently-resolved package looks like, then relinks |
971
- | `npx forgedock update` (git-clone install) | **Does not** touch `~/.forge/` at all — see the exemption below |
972
- | `npx forgedock doctor` | Reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` exist |
971
+ | `npx forgedock update` (git-clone install) | Updates and relinks a `main` checkout; a non-main checkout is left untouched and gets actionable guidance because the current source must remain the source used after the command returns |
972
+ | `npx forgedock doctor` | Reports the effective source path, version, and git branch, then reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` for npm installs |
973
973
 
974
974
  The copy is idempotent: files whose content is already byte-identical are never rewritten, so steady-state re-runs are fast and don't generate false "updated" noise.
975
975
 
@@ -979,6 +979,7 @@ If you run ForgeDock from a git clone (or a git worktree) of the repo itself —
979
979
 
980
980
  - `~/.claude/commands/` continues to symlink directly from the clone, exactly as before this feature.
981
981
  - `~/.forge/{bin,commands,scripts,templates}` correctly does **not** exist for this install mode — `npx forgedock doctor` treats that absence as healthy, not a warning.
982
+ - `npx forgedock update` never switches a non-main checkout to `main` and then restores it. That transient update would leave the next invocation running the old branch source, so the command reports the effective path, branch, and version and tells you to run from a main checkout instead.
982
983
 
983
984
  ### Not the Same as `~/.forge/{runs,index}`
984
985
 
@@ -1086,7 +1087,7 @@ The directory (`~/.forge/`) is created automatically on first write with `mkdir(
1086
1087
  ### When It's Written
1087
1088
 
1088
1089
  - After every successful `npx forgedock install` (end of the onboarding journey, right after the "Forged." celebration screen).
1089
- - After every `npx forgedock update` — both the git-clone branch (fast-forward pull) and the npm branch (version-check advisory) route through the same repair step (`relinkAndHint()`), so the receipt is refreshed either way.
1090
+ - After every successful `npx forgedock update` — the main git-clone branch and npm branch route through the same repair step (`relinkAndHint()`), so the receipt is refreshed either way. A non-main git-clone advisory intentionally does not relink or rewrite the receipt.
1090
1091
 
1091
1092
  The write is best-effort and never blocks or fails the install/update it records: any error (permission denied, disk full, unusual `FORGE_HOME` layout) degrades silently to a no-op, matching the fail-open contract already used by the command-manifest and hook-registration steps.
1092
1093