forgedock 1.7.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.
package/bin/runner.mjs CHANGED
@@ -127,6 +127,35 @@ function readOnlySet(values) {
127
127
  * simultaneously. Treat it as immutable; mutation attempts throw.
128
128
  */
129
129
  export const VALID_BACKENDS = readOnlySet(["cli", "api", "auto"]);
130
+
131
+ export const FORGE_OPENCODE_CAPABILITY_ERROR = "FORGE_OPENCODE_CAPABILITY_ERROR";
132
+
133
+ /**
134
+ * Whether the caller is an OpenCode session. The adapter sets this marker on
135
+ * every shell environment; keeping the predicate small makes all Claude
136
+ * backend entrypoints agree on the same runtime boundary.
137
+ */
138
+ export function isOpenCodeRuntime(env = process.env) {
139
+ return String(env?.FORGE_RUNTIME || "").toLowerCase() === "opencode";
140
+ }
141
+
142
+ /**
143
+ * Refuse Claude-backed execution from an OpenCode workflow. OpenCode has its
144
+ * own Skill/Task execution path; falling through to this runner would create a
145
+ * competing controller and may consume Claude credentials or quota.
146
+ */
147
+ export function assertOpenCodeNativeRuntime({
148
+ env = process.env,
149
+ operation = "Claude-backed ForgeDock execution",
150
+ } = {}) {
151
+ if (!isOpenCodeRuntime(env)) return;
152
+ const error = new Error(
153
+ `${FORGE_OPENCODE_CAPABILITY_ERROR}: ${operation} is unavailable when FORGE_RUNTIME=opencode. Use native ForgeDock Skill/Task dispatch instead.`,
154
+ );
155
+ error.code = FORGE_OPENCODE_CAPABILITY_ERROR;
156
+ throw error;
157
+ }
158
+
130
159
  // Per-process memoization for isClaudeCliAvailable(), keyed by `cwd` (issue
131
160
  // #2011). `runCommand()` calls resolveBackend() — and therefore, for the
132
161
  // default "auto" backend, isClaudeCliAvailable() — unconditionally on every
@@ -491,9 +520,10 @@ export function runCliWithStalePathRecovery({
491
520
  * API checks verify that a key is configured, not that the key is accepted.
492
521
  */
493
522
  export function checkExecutionBackend({
494
- requested = process.env.FORGEDOCK_BACKEND || "auto",
523
+ env = process.env,
524
+ requested = env.FORGEDOCK_BACKEND || "auto",
495
525
  cwd = process.cwd(),
496
- apiKey = process.env.ANTHROPIC_API_KEY,
526
+ apiKey = env.ANTHROPIC_API_KEY,
497
527
  resolveCliFn = resolveClaudeCliBinary,
498
528
  spawnImpl = spawnSync,
499
529
  sdkAvailableFn = () => {
@@ -509,6 +539,10 @@ export function checkExecutionBackend({
509
539
  return { ready: false, backend: requested, reason: "invalid-backend" };
510
540
  }
511
541
 
542
+ if (isOpenCodeRuntime(env)) {
543
+ return { ready: false, backend: "opencode", reason: "native-opencode-required" };
544
+ }
545
+
512
546
  if (requested !== "api") {
513
547
  const cliPath = resolveCliFn(cwd);
514
548
  if (cliPath) {
@@ -606,14 +640,16 @@ export function resolveBackendLadder({
606
640
  * @param {object} [opts]
607
641
  * @param {string} [opts.requested] - "cli" | "api" | "auto" (default "auto").
608
642
  * @param {string} [opts.cwd] - Working directory for the CLI probe.
643
+ * @param {object} [opts.env] - Runtime environment used for capability checks.
609
644
  * @returns {"cli"|"api"}
610
645
  */
611
- export function resolveBackend({ requested = "auto", cwd = process.cwd() } = {}) {
646
+ export function resolveBackend({ requested = "auto", cwd = process.cwd(), env = process.env } = {}) {
612
647
  if (!VALID_BACKENDS.has(requested)) {
613
648
  throw new Error(
614
649
  `Invalid backend "${requested}". Must be one of: ${[...VALID_BACKENDS].join(", ")}.`,
615
650
  );
616
651
  }
652
+ assertOpenCodeNativeRuntime({ env, operation: "Claude-backed backend resolution" });
617
653
  return resolveBackendLadder({
618
654
  override: requested === "auto" ? undefined : requested,
619
655
  validOverrides: new Set(["cli", "api"]),
@@ -2086,9 +2122,15 @@ export async function runCommand(opts = {}) {
2086
2122
  maxIterations = DEFAULT_MAX_ITERATIONS,
2087
2123
  dryRun = false,
2088
2124
  backend = process.env.FORGEDOCK_BACKEND || "auto",
2125
+ runtimeEnv = process.env,
2089
2126
  logger = console,
2090
2127
  } = opts;
2091
2128
 
2129
+ assertOpenCodeNativeRuntime({
2130
+ env: runtimeEnv,
2131
+ operation: "Claude-backed ForgeDock runner",
2132
+ });
2133
+
2092
2134
  const spec = loadCommandSpec(commandsDir, commandName);
2093
2135
  const systemPrompt = buildSystemPrompt(spec, { repoRoot: cwd });
2094
2136
  // CLI-backend-specific prompt (issue #2019) — see buildCliSystemPrompt's
@@ -2104,10 +2146,10 @@ export async function runCommand(opts = {}) {
2104
2146
  // misbehaving. An explicitly invalid `backend` value DOES throw here —
2105
2147
  // surfacing a bad --backend/FORGEDOCK_BACKEND value immediately, before any
2106
2148
  // work is attempted, rather than silently ignoring it.
2107
- let resolvedBackend = resolveBackend({ requested: backend, cwd });
2149
+ let resolvedBackend = resolveBackend({ requested: backend, cwd, env: runtimeEnv });
2108
2150
 
2109
2151
  if (resolvedBackend === "cli" && backend === "auto") {
2110
- const readiness = checkExecutionBackend({ requested: "auto", cwd, apiKey });
2152
+ const readiness = checkExecutionBackend({ requested: "auto", cwd, apiKey, env: runtimeEnv });
2111
2153
  if (readiness.backend === "api") resolvedBackend = "api";
2112
2154
  }
2113
2155
 
@@ -1145,8 +1145,15 @@ if [ -n "${OPENCODE_SESSION_ID:-}" ] || [ -n "${OPENCODE_PID:-}" ] || [ -n "${OP
1145
1145
  fi
1146
1146
 
1147
1147
  if [ "$FORGE_RUNTIME" = "opencode" ]; then
1148
- echo "OpenCode runtime detected: native task dispatch selected; Claude background dispatch is not required."
1149
- BACKGROUND_DISPATCH_ENABLED=false
1148
+ echo "OpenCode runtime detected: native task dispatch selected."
1149
+ # ForgeDock's OpenCode plugin opts into background task events by default so
1150
+ # each completed DAG node can wake this parent without a wave barrier.
1151
+ if [ "${OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS:-true}" = "false" ]; then
1152
+ echo "OpenCode background subagents explicitly disabled: orchestration is degraded to foreground task dispatch."
1153
+ BACKGROUND_DISPATCH_ENABLED=false
1154
+ else
1155
+ BACKGROUND_DISPATCH_ENABLED=true
1156
+ fi
1150
1157
  fi
1151
1158
 
1152
1159
  # Feature gate check — run once before Phase 4 dispatch begins
@@ -1176,13 +1183,23 @@ fi
1176
1183
 
1177
1184
  When `FORGE_RUNTIME=opencode` (or an OpenCode runtime marker is present), do not
1178
1185
  interpret a missing `claude` executable as an environment failure. The Phase 4
1179
- dispatcher must use OpenCode's inline continuation contract instead of the
1180
- Claude `Agent(...)` fallback. Claude remains the default when no runtime marker
1181
- is present, preserving the existing Claude behavior.
1182
-
1183
- **When `BACKGROUND_DISPATCH_ENABLED=false`**: fall back to the current streaming dispatch behavior — `run_in_background=true` is still set on each `Agent()` call (existing behavior, already correct), but treat completions as synchronous and do not rely on `agent_completed` notifications. Poll issue labels for terminal state instead.
1184
-
1185
- **When `BACKGROUND_DISPATCH_ENABLED=true`**: use `run_in_background=true` (already in the Step 4A Agent() template) and react to `agent_completed` notifications as documented in Step 4B. Do NOT poll.
1186
+ dispatcher must use OpenCode's native `task` contract instead of the Claude
1187
+ `Agent(...)` fallback. With background subagents enabled, each task is launched
1188
+ with `background=true` and its injected task-result event immediately drives
1189
+ predecessor classification and newly-ready dispatch. Claude remains the default
1190
+ when no runtime marker is present, preserving the existing Claude behavior.
1191
+
1192
+ **When `BACKGROUND_DISPATCH_ENABLED=false`**: Claude falls back to its existing
1193
+ synchronous/polling behavior. OpenCode reaches this branch only after an
1194
+ explicit `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false`; use independent
1195
+ foreground `task` calls where the host can execute them concurrently, and
1196
+ report that per-completion streaming is unavailable. Do not use Claude's
1197
+ `Agent(...)` fallback and do not claim the event-driven DAG guarantee.
1198
+
1199
+ **When `BACKGROUND_DISPATCH_ENABLED=true`**: Claude uses
1200
+ `run_in_background=true` and reacts to `agent_completed` notifications. OpenCode
1201
+ uses `task(..., background=true)` and reacts to each injected task-result event.
1202
+ Do NOT poll or wait for a whole wave in either enabled path.
1186
1203
 
1187
1204
  ### Orchestrator state reconstruction on wake / after compaction
1188
1205
 
@@ -416,6 +416,9 @@ declare -A AGENT_ISSUE_MAP
416
416
  # this map to identify which issue a background-Bash completion notification belongs to,
417
417
  # the same role AGENT_ISSUE_MAP plays for Agent-tool `agent_completed` notifications.
418
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
419
422
 
420
423
  # Same-file current-state brief forwarding (forge#1860). Populated by the core streaming
421
424
  # dispatch loop below (Step 4B) whenever a Layer 1/2/3 structural predecessor edge (see
@@ -556,10 +559,33 @@ and continue until it reaches
556
559
  `workflow:merged`, `workflow:invalid`, `needs-human`, or
557
560
  `workflow:awaiting-merge`.
558
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
+
559
584
  ```bash
560
585
  if [ "${FORGE_RUNTIME:-}" = "opencode" ]; then
561
586
  echo "OpenCode native task dispatch selected — load commands/work-on.md for each ready issue."
562
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."
563
589
  echo "If native task dispatch cannot run, post FORGE:OPENCODE_BLOCKED with the missing capability and add needs-human."
564
590
  fi
565
591
  ```
@@ -567,6 +593,13 @@ fi
567
593
  This branch is additive. When OpenCode is not selected, continue through the
568
594
  existing engine-first and Claude Agent-spawn paths below without modification.
569
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
+
570
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.
571
604
 
572
605
  ```bash
@@ -687,7 +720,13 @@ ENGINE_DISPATCH_MAP[{NUM}] = <task_id returned by Bash(run_in_background=true, .
687
720
 
688
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.
689
722
 
690
- **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.
691
730
 
692
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.**
693
732
 
@@ -917,9 +956,9 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle
917
956
 
918
957
  ### Step 4B: Monitor completions and dispatch newly ready issues
919
958
 
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).
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.
921
960
 
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.)
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.)
923
962
 
924
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.
925
964
 
@@ -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
 
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
 
package/docs/OPENCODE.md CHANGED
@@ -19,12 +19,12 @@ For the optional command tier:
19
19
  npx forgedock opencode install --extras
20
20
  ```
21
21
 
22
- Restart OpenCode after install or update. OpenCode loads commands and plugins
23
- at startup.
22
+ Restart OpenCode after install or update. OpenCode loads commands, skills, and
23
+ plugins at startup.
24
24
 
25
- The installer does not edit `opencode.json` or `opencode.jsonc`. It writes only
26
- ForgeDock-owned files under OpenCode's config directory and records them in
27
- `forgedock/manifest.json` for safe updates and removal.
25
+ The native adapter does not add or modify user-owned OpenCode settings. It
26
+ writes only ForgeDock-owned files under OpenCode's config directory and records
27
+ them in `forgedock/manifest.json` for safe updates and removal.
28
28
 
29
29
  ## Usage
30
30
 
@@ -62,9 +62,29 @@ The generated plugin has no prompt text. It:
62
62
  - injects `FORGE_HOME` into OpenCode shell environments;
63
63
  - defaults `subagent_depth` to 2 when the user has not configured it, while
64
64
  preserving explicit lower limits;
65
+ - grants the built-in `general` subagent permission to invoke native `task`
66
+ unless the user explicitly configured a task permission;
67
+ - opts into OpenCode background subagents by default so each completed issue can
68
+ wake the parent orchestrator independently; set
69
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` to opt out;
65
70
  - selects Git Bash on Windows only when the user has not explicitly configured
66
71
  an OpenCode shell, because the shared workflows and helper scripts use Bash.
67
72
 
73
+ The adapter also registers every eligible ForgeDock workflow as a native
74
+ OpenCode skill. Top-level and nested source paths are normalized to valid
75
+ hyphenated skill names while the wrapper continues to point at the authoritative
76
+ source file:
77
+
78
+ ```text
79
+ commands/work-on.md -> skills/work-on/SKILL.md
80
+ commands/work-on/investigate.md -> skills/work-on-investigate/SKILL.md
81
+ commands/review-pr.md -> skills/review-pr/SKILL.md
82
+ ```
83
+
84
+ The wrapper is intentionally thin. It carries the workflow's `name` and
85
+ `description` frontmatter, preserves the current-context arguments, and loads
86
+ only the referenced `commands/**/*.md` spec.
87
+
68
88
  OpenCode's provider configuration remains entirely user-owned. ForgeDock does
69
89
  not require Anthropic when invoked through OpenCode; any provider and model
70
90
  supported by the user's OpenCode configuration can execute the commands.
@@ -74,19 +94,45 @@ supported by the user's OpenCode configuration can execute the commands.
74
94
  The adapter follows these rules:
75
95
 
76
96
  - No global ForgeDock `instructions` entry.
77
- - No generated skill for every source file. Large skill catalogs add metadata
78
- to every session even when ForgeDock is unused.
79
- - Only top-level user entry commands are registered.
97
+ - Top-level user entry commands are registered under `/forge/*`.
98
+ - Every eligible workflow is registered as a native skill so nested
99
+ `Skill(...)` dispatch resolves through OpenCode's `skill` tool instead of
100
+ filesystem guessing.
80
101
  - Nested phase specs are loaded only when their dispatcher reaches them.
81
102
  - Task/Agent work uses OpenCode subagents only for the parallelism, isolation,
82
103
  or context-pressure cases required by the shared workflow.
104
+ - Orchestration dispatches independent issues with `task(background=true)` and
105
+ processes each injected task-result event immediately; it does not wait for a
106
+ wave or the slowest sibling.
83
107
  - GitHub state and the durable engine remain the recovery source instead of
84
108
  replaying prior prompt context.
85
109
 
86
110
  The generated adapter preamble is intentionally small. It maps Claude Code's
87
- in-conversation `Skill(...)` loading to a lazy read of the corresponding shared
88
- spec. It maps isolated `Task(...)` and permitted `Agent(...)` calls to
89
- OpenCode's native `task` tool.
111
+ in-conversation `Skill(...)` loading to the normalized native skill name and
112
+ authoritative shared spec. It maps isolated `Task(...)` and permitted
113
+ `Agent(...)` calls to OpenCode's native `task` tool. Before the shared review
114
+ specs evaluate Claude's literal `Task`/`Agent` names, an OpenCode runtime marker
115
+ selects `DISPATCH_TOOL=task`; the absence of those Claude names must not produce
116
+ a false `FORGE:REVIEW_BLOCKED` result.
117
+
118
+ Every isolated review dispatch uses the explicit native argument shape. Every
119
+ native task call must include the schema-required `subagent_type` at the top
120
+ level alongside its `description` and `prompt`:
121
+
122
+ ```js
123
+ { description: "...", prompt: "...", subagent_type: "general", background: true }
124
+ ```
125
+
126
+ Implementation and review work uses `general`; read-only discovery uses
127
+ `explore`. Claude `general-purpose` maps to `general` and
128
+ `codebase-explorer` maps to `explore`. When a source task omits its type, the
129
+ generated adapter safely defaults it to `general`; unsupported types stop with
130
+ `FORGE_OPENCODE_CAPABILITY_ERROR` instead of reaching schema validation.
131
+ If the native `task` capability itself is unavailable, the workflow posts
132
+ `FORGE:REVIEW_BLOCKED` and stops rather than falling back to inline review or
133
+ another pipeline controller.
134
+ The generated plugin adds `background: true` unless
135
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is explicitly set.
90
136
 
91
137
  `commands/work-on.md` is still a large entry dispatcher and is loaded in full,
92
138
  matching the Claude Code path. The adapter prevents additional eager loading,
@@ -102,13 +148,27 @@ npx forgedock opencode install --extras
102
148
  npx forgedock opencode uninstall
103
149
  ```
104
150
 
151
+ `npx forgedock update` also refreshes an existing managed OpenCode adapter,
152
+ including its plugin and generated skills. It preserves the installed core vs.
153
+ `--extras` tier, migrates the sentinel-marked legacy adapter, and does not
154
+ install OpenCode files when no ForgeDock adapter is already registered.
155
+
105
156
  Updates are deterministic and prune stale ForgeDock-owned command files.
106
157
  Uninstall removes only files listed in the ownership manifest and still marked
107
158
  with a ForgeDock sentinel. User-owned commands, plugins, and files placed in
108
159
  the `forgedock/` namespace are never removed; that namespace is pruned only
109
160
  when it is empty.
110
- Install also removes the previous ForgeDock-managed `~/.opencode-forge.md`
111
- instructions file and its exact legacy config entries when they are present.
161
+ For backward compatibility, install and uninstall may rewrite `opencode.json`
162
+ only to remove legacy entries whose two-key `description` and `template` still
163
+ exactly match one of the definitions emitted by the retired adapter and point to
164
+ the active ForgeDock home (or the home recorded in the ownership manifest during
165
+ uninstall), and the sentinel-marked legacy instructions file is present. It also
166
+ removes references to that managed `~/.opencode-forge.md` instructions file. The
167
+ migration does not rewrite `opencode.jsonc`. User-owned settings and customized commands are preserved;
168
+ definitions with extra keys, edited fields, different
169
+ paths, a missing ownership marker, or ambiguous ownership are left in place. If
170
+ a legacy config cannot be parsed or written, the migration leaves the legacy
171
+ artifacts in place.
112
172
 
113
173
  ## Locations
114
174
 
@@ -117,6 +177,7 @@ Default global location:
117
177
  ```text
118
178
  ~/.config/opencode/
119
179
  commands/forge/*.md
180
+ skills/<workflow-name>/SKILL.md
120
181
  plugins/forgedock.js
121
182
  forgedock/manifest.json
122
183
  ```
@@ -133,16 +194,25 @@ The separate `forgedock run` and `forgedock run-issue` backend still supports
133
194
  only Claude CLI and the Anthropic API. An OpenCode engine backend must be added
134
195
  and validated before ForgeDock can claim provider-neutral headless parity.
135
196
 
136
- Claude's `PreToolUse` safety hook has not yet been ported to an OpenCode plugin.
137
- The shared workflow rules and deterministic scripts still apply, but equivalent
138
- mechanical enforcement must be implemented and tested before claiming complete
139
- Claude-runtime safety parity.
140
-
141
- OpenCode 1.18.4 keeps background subagents experimental. When they are not
142
- enabled, the command adapter requires independent foreground tasks to be
143
- launched concurrently where supported and uses ForgeDock's GitHub-label polling
144
- fallback. This preserves isolated review contexts but does not yet prove equal
145
- wall-clock behavior for `/forge/orchestrate`.
197
+ The ForgeDock plugin provides the OpenCode-specific runtime boundary for shell
198
+ and task execution. Its `tool.execute.before` hook rejects Claude-backed and
199
+ recursive ForgeDock controller commands, and normalizes native task arguments
200
+ before execution; the shared workflow rules and deterministic scripts remain
201
+ the source of truth for all other behavior.
202
+
203
+ OpenCode 1.18.4 keeps background subagents experimental. ForgeDock opts into
204
+ that feature by default because the streaming DAG depends on the parent session
205
+ receiving one completion event per child. A native background task returns a
206
+ `<task id="..." state="running">` marker immediately and later injects a
207
+ `state="completed"` or `state="error"` result into the parent session. The
208
+ orchestrator maps that id to the issue, re-reads GitHub state, and dispatches
209
+ newly unlocked successors in the same response.
210
+
211
+ If `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is explicitly set,
212
+ ForgeDock uses independent foreground tasks where OpenCode can execute them
213
+ concurrently and labels the run as degraded. Foreground tasks cannot provide
214
+ the same per-completion wake behavior, so this opt-out intentionally restores a
215
+ wave-like fallback rather than silently claiming Claude-equivalent throughput.
146
216
 
147
217
  Commands that inspect Claude-specific transcripts or Claude installation state
148
218
  remain runtime-specific and should not be represented as portable until they
@@ -151,7 +221,10 @@ receive dedicated implementations.
151
221
  ## Orchestration Runtime
152
222
 
153
223
  OpenCode orchestration uses the native OpenCode `task` tool for isolated issue
154
- work and follows the shared `commands/work-on.md` state machine. It must not
224
+ work and follows the shared `commands/work-on.md` state machine. Each ready
225
+ issue is launched as `task(subagent_type="general", background=true)` and its
226
+ task-result event is treated as the completion notification for that issue. It
227
+ must not
155
228
  route through `forgedock run-issue` when that would select the Claude CLI or
156
229
  Anthropic API backend. If the host does not expose an OpenCode runtime marker,
157
230
  set the runtime explicitly before launching a headless command:
@@ -162,10 +235,25 @@ FORGE_RUNTIME=opencode opencode run --command forge/orchestrate "fast-lane"
162
235
 
163
236
  Each task re-reads GitHub labels and `FORGE:*` comments after every phase and
164
237
  continues until `workflow:merged`, `workflow:invalid`, `needs-human`, or
165
- `workflow:awaiting-merge`. If native task dispatch is unavailable, post a
238
+ `workflow:awaiting-merge`. The parent orchestrator processes every child
239
+ completion independently, so a completed predecessor can unlock and dispatch
240
+ its successors while unrelated issues continue running. If native task
241
+ dispatch is unavailable, post a
166
242
  `FORGE:OPENCODE_BLOCKED` diagnostic naming the missing capability and add
167
243
  `needs-human`; do not leave an issue stranded at `workflow:engine-error`.
168
244
 
245
+ If a generated command or skill cannot load its authoritative workflow, it must
246
+ stop with an actionable `FORGE_OPENCODE_CAPABILITY_ERROR`. It must not recover
247
+ by invoking `forgedock run-issue`, `npx forgedock run-issue`, or a recursive
248
+ `opencode run`; those paths select a competing controller or Claude-backed
249
+ backend.
250
+
251
+ The same boundary is enforced below the plugin for direct CLI callers. With
252
+ `FORGE_RUNTIME=opencode`, `forgedock run`, `forgedock run-issue`, and Claude
253
+ backend preflight fail before selecting a provider. The stable error is
254
+ `FORGE_OPENCODE_CAPABILITY_ERROR`; continue by using the registered native
255
+ Skill and Task workflow instead.
256
+
169
257
  This runtime branch is additive. Claude keeps its existing engine and
170
258
  background-agent paths, and Codex keeps its installed namespaced skills and
171
259
  repo-local adapters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"