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.
- package/README.md +14 -2
- package/bin/engine/orchestrate-canary.mjs +15 -0
- package/bin/engine/orchestrate-canary.test.mjs +28 -0
- package/bin/engine-cli.mjs +2 -0
- package/bin/forgedock.mjs +176 -44
- package/bin/opencode-adapter.mjs +961 -0
- package/bin/registry.mjs +2 -1
- package/bin/runner.mjs +250 -60
- package/commands/orchestrate/phase-3-dependency.md +43 -6
- package/commands/orchestrate/phase-4-execution.md +158 -58
- package/commands/review-pr-agents.md +8 -0
- package/commands/review-pr-staging.md +18 -0
- package/commands/review-pr.md +20 -2
- package/commands/work-on/build.md +10 -1
- package/commands/work-on.md +10 -1
- package/docs/CONFIG.md +6 -5
- package/docs/OPENCODE.md +238 -270
- package/package.json +3 -2
|
@@ -352,14 +352,39 @@ This step is the deterministic counterpart to fix #2 (atomic create-if-absent in
|
|
|
352
352
|
Before building agent prompts, run `classify-lane.sh` for every issue in the current dispatch group to compute `{LANE}` and `{PR_BASE}` deterministically. The script output is authoritative — the LLM MUST NOT override or reason around it.
|
|
353
353
|
|
|
354
354
|
```bash
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
# Resolve the helper once and reuse it for the initial dispatch and all finding
|
|
356
|
+
# classification loops. Claude keeps its installed path as the default, while
|
|
357
|
+
# OpenCode and Codex can use ForgeDock's repository-local scripts.
|
|
358
|
+
resolve_classify_lane() {
|
|
359
|
+
local candidates=()
|
|
360
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
361
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
362
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
363
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
364
|
+
[ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
|
|
365
|
+
[ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
|
|
366
|
+
candidates+=("$HOME/.opencode/scripts/classify-lane.sh")
|
|
367
|
+
else
|
|
368
|
+
[ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
|
|
369
|
+
candidates+=("$HOME/.claude/scripts/classify-lane.sh")
|
|
370
|
+
[ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
local candidate
|
|
374
|
+
for candidate in "${candidates[@]}"; do
|
|
375
|
+
if [ -f "$candidate" ]; then
|
|
376
|
+
printf '%s\n' "$candidate"
|
|
377
|
+
return 0
|
|
378
|
+
fi
|
|
379
|
+
done
|
|
380
|
+
echo "ERROR: classify-lane.sh is not installed in any configured runtime path." >&2
|
|
381
|
+
return 1
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
CLASSIFY_LANE_SCRIPT=$(resolve_classify_lane) || {
|
|
385
|
+
echo "ERROR: cannot classify lanes without classify-lane.sh" >&2
|
|
386
|
+
exit 1
|
|
387
|
+
}
|
|
363
388
|
|
|
364
389
|
declare -A ISSUE_LANE
|
|
365
390
|
declare -A ISSUE_PR_BASE
|
|
@@ -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
|
|
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
|
-
#
|
|
540
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
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 !
|
|
552
|
-
echo "WARNING: forgedock CLI is on PATH, but
|
|
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
|
|
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.
|
|
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`,
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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 [ "$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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 -
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
|
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
|
|
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.
|
package/commands/review-pr.md
CHANGED
|
@@ -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
|
-
|
|
128
|
+
WORKTREE_ROOT="/path/to/repo/.claude/worktrees"
|
|
129
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
130
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
131
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
132
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
133
|
+
WORKTREE_ROOT="/path/to/repo/.opencode/worktrees"
|
|
134
|
+
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
135
|
+
WORKTREE_ROOT="/path/to/repo/.codex/worktrees"
|
|
136
|
+
fi
|
|
137
|
+
WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
|
|
129
138
|
git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
|
|
130
139
|
```
|
|
131
140
|
|
package/commands/work-on.md
CHANGED
|
@@ -1411,7 +1411,16 @@ esac
|
|
|
1411
1411
|
cd {REPO_PATH}
|
|
1412
1412
|
git fetch origin
|
|
1413
1413
|
BRANCH="fix/{slug}-{NUMBER}"
|
|
1414
|
-
|
|
1414
|
+
WORKTREE_ROOT="{REPO_PATH}/.claude/worktrees"
|
|
1415
|
+
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
1416
|
+
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
1417
|
+
[ -n "${OPENCODE_PID:-}" ] ||
|
|
1418
|
+
[ -n "${OPENCODE:-}" ]; then
|
|
1419
|
+
WORKTREE_ROOT="{REPO_PATH}/.opencode/worktrees"
|
|
1420
|
+
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
1421
|
+
WORKTREE_ROOT="{REPO_PATH}/.codex/worktrees"
|
|
1422
|
+
fi
|
|
1423
|
+
WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
|
|
1415
1424
|
git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
|
|
1416
1425
|
```
|
|
1417
1426
|
|
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
|
-
|
|
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) |
|
|
972
|
-
| `npx forgedock doctor` | Reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}`
|
|
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` —
|
|
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
|
|