forgedock 1.16.0 → 1.16.2
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/engine/phases.mjs +69 -15
- package/bin/engine/runlog.mjs +21 -3
- package/bin/engine.mjs +43 -7
- package/bin/runner.mjs +21 -2
- package/commands/forge-stats.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +6 -40
- package/commands/orchestrate/phase-4-execution.md +26 -162
- package/commands/quality-gate.md +12 -80
- package/commands/review-pr-staging.md +67 -301
- package/commands/review-pr.md +150 -752
- package/commands/work-on/build/validate.md +6 -40
- package/commands/work-on/build.md +82 -40
- package/commands/work-on/close.md +7 -22
- package/commands/work-on/decompose.md +8 -24
- package/commands/work-on/investigate.md +18 -29
- package/commands/work-on/remediate.md +38 -147
- package/commands/work-on/review.md +25 -42
- package/commands/work-on.md +13 -63
- package/docs/CONFIG.md +10 -7
- package/docs/RELEASE-NOTES.md +6 -0
- package/docs/WORK-ON-RUNTIME.md +1 -1
- package/docs/spec/forge-protocol-v1.md +1 -1
- package/package.json +1 -1
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-spec-size.sh +47 -27
- package/scripts/check-spec-size.test.sh +46 -3
- package/scripts/diff-size.sh +36 -3
- package/scripts/diff-size.test.sh +47 -0
- package/scripts/forge-root.test.sh +218 -91
- package/scripts/lib/fence-bootstrap.sh +16 -0
- package/scripts/lib/forge-resolve.sh +96 -0
- package/scripts/lib/review-delta-mode.sh +41 -0
- package/scripts/lint-dispatch-prompt.test.sh +3 -3
- package/scripts/remediate-liveness.sh +96 -0
- package/scripts/review-acceptance-gate.sh +132 -0
- package/scripts/review-acceptance-gate.test.sh +90 -0
- package/scripts/review-pr-delta-wiring.test.sh +24 -7
- package/scripts/review-pr-staging-scope.test.sh +14 -7
- package/scripts/spec-no-worktree-script.test.sh +1 -1
|
@@ -69,31 +69,14 @@ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
|
69
69
|
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
70
70
|
ADAPTIVE_ENABLED=false
|
|
71
71
|
fi
|
|
72
|
-
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the
|
|
72
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the shared forge_resolve stub
|
|
73
73
|
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
74
74
|
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
75
|
-
# Resolution: $FORGEDOCK_HOME (authoritative when set) >
|
|
75
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > the running plugin root (FORGE_SOURCED_ROOT) > $FORGE_HOME > the
|
|
76
76
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
77
|
-
# FORGE_ROOT
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
81
|
-
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
82
|
-
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
83
|
-
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
84
|
-
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
85
|
-
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
86
|
-
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
87
|
-
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
88
|
-
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
89
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
90
|
-
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
91
|
-
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
92
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
93
|
-
while IFS= read -r _c; do
|
|
94
|
-
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
95
|
-
done <<< "$_k"
|
|
96
|
-
fi
|
|
77
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
78
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
79
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
97
80
|
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
98
81
|
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
99
82
|
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
@@ -331,26 +314,9 @@ A PR must not be opened for work whose earlier phases were skipped. Run the dete
|
|
|
331
314
|
# --no-renames: list BOTH sides of a rename so a file moved into docs/ cannot hide its source path (forge#3145).
|
|
332
315
|
CHANGED=$(git -C {WORKTREE_PATH} diff --name-only --no-renames origin/{PR_BASE}...HEAD)
|
|
333
316
|
# The verifier ships with ForgeDock (not the consumer repo): same resolution as every universal script.
|
|
334
|
-
# FORGE_ROOT
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
338
|
-
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
339
|
-
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
340
|
-
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
341
|
-
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
342
|
-
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
343
|
-
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
344
|
-
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
345
|
-
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
346
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
347
|
-
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
348
|
-
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
349
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
350
|
-
while IFS= read -r _c; do
|
|
351
|
-
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
352
|
-
done <<< "$_k"
|
|
353
|
-
fi
|
|
317
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
318
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
319
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
354
320
|
TRAIL_SCRIPT="$FORGE_ROOT/scripts/verify-phase-trail.sh"
|
|
355
321
|
# Docs-only predicate: ONE shared copy (scripts/is-docs-only.sh, forge#3134). Fail closed: unresolved script or empty diff -> no flag.
|
|
356
322
|
DOCS_ONLY_FLAG=""
|
|
@@ -480,10 +446,27 @@ esac
|
|
|
480
446
|
|
|
481
447
|
---
|
|
482
448
|
|
|
449
|
+
## Phase R2.5: Review Acceptance Gate <!-- Added: forge#3793 -->
|
|
450
|
+
|
|
451
|
+
Evaluates the `phase=review` acceptance checks B6.5 deferred (no PR existed at build time) before `/review-pr --auto-merge`: after R2D, and at the top of R3 on resume. Idempotent per head; a no-op without deferred checks; own marker `FORGE:ACCEPTANCE_GATE_REVIEW`, never the build gate's sentinels.
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
# <Script resolution block, verbatim>
|
|
455
|
+
RESOLUTION=$(resolve_script 'review-acceptance-gate'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
456
|
+
[ "$TIER" != "prose" ] || { echo "review-acceptance-gate not resolvable (fail closed)"; exit 1; }
|
|
457
|
+
bash "$SCRIPT_PATH" {NUMBER} {GH_REPO} "{GH_FLAG}" "{WORKTREE_PATH}" || exit 1
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
On non-zero exit do NOT invoke `/review-pr`; the script posted FAILED, added `needs-human` and printed the `REVIEW_RESULT: status: BLOCKED` block as the final reply.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
483
464
|
## Phase R3: Invoke /review-pr with --auto-merge
|
|
484
465
|
|
|
485
466
|
**First re-run the Phase R1.5 phase-trail preflight** (resume entry can skip R1.5). Do not invoke `/review-pr` while it fails. <!-- Added: forge#3061 -->
|
|
486
467
|
|
|
468
|
+
Then re-run **Phase R2.5** (the review acceptance gate); do not invoke `/review-pr` while it fails. <!-- Added: forge#3793 -->
|
|
469
|
+
|
|
487
470
|
Re-read the PR number (from creation or from resume check):
|
|
488
471
|
|
|
489
472
|
```bash
|
package/commands/work-on.md
CHANGED
|
@@ -231,26 +231,9 @@ Extract project prefix and issue number. If `next`/`pick`: list open issues sort
|
|
|
231
231
|
**Spawn-depth preflight (before 0A.1 and before any GitHub write)** <!-- forge#3398 -->: confirm the dispatching phases will have the `Agent` tool (Depth Budget). The router runs at layer 1 under `/orchestrate`, else layer 0. The block is self-contained (it carries the canonical `FORGE_ROOT` bootstrap).
|
|
232
232
|
|
|
233
233
|
```bash
|
|
234
|
-
# FORGE_ROOT
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
238
|
-
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
239
|
-
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
240
|
-
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
241
|
-
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
242
|
-
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
243
|
-
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
244
|
-
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
245
|
-
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
246
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
247
|
-
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
248
|
-
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
249
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
250
|
-
while IFS= read -r _c; do
|
|
251
|
-
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
252
|
-
done <<< "$_k"
|
|
253
|
-
fi
|
|
234
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
235
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
236
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
254
237
|
ROUTER_LAYER=0; [ "$UNDER_ORCHESTRATION" = "true" ] && ROUTER_LAYER=1
|
|
255
238
|
if [ -n "$FORGE_ROOT" ] && [ -f "$FORGE_ROOT/scripts/spawn-depth-check.sh" ]; then
|
|
256
239
|
DEPTH_OUT=$(bash "$FORGE_ROOT/scripts/spawn-depth-check.sh" --router-layer "$ROUTER_LAYER"); DEPTH_RC=$?
|
|
@@ -320,26 +303,9 @@ A `/recover-orphans` sweep that resumes this issue inline holds an issue-scoped
|
|
|
320
303
|
|
|
321
304
|
```bash
|
|
322
305
|
# Shell state does not persist: the bootstrap is repeated here.
|
|
323
|
-
# FORGE_ROOT
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
327
|
-
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
328
|
-
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
329
|
-
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
330
|
-
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
331
|
-
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
332
|
-
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
333
|
-
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
334
|
-
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
335
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
336
|
-
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
337
|
-
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
338
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
339
|
-
while IFS= read -r _c; do
|
|
340
|
-
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
341
|
-
done <<< "$_k"
|
|
342
|
-
fi
|
|
306
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
307
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
308
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
343
309
|
RECOVERY_SWEEP_ID=$(printf '%s' "$ARGUMENTS" | sed -n 's/.*--recovery-sweep[[:space:]][[:space:]]*\([^[:space:]][^[:space:]]*\).*/\1/p' | head -1)
|
|
344
310
|
if [ -n "$FORGE_ROOT" ] && [ -f "$FORGE_ROOT/scripts/recovery-claim-live.sh" ]; then
|
|
345
311
|
CLAIM_OUT=$(bash "$FORGE_ROOT/scripts/recovery-claim-live.sh" {NUMBER} -R {GH_REPO} ${RECOVERY_SWEEP_ID:+--exempt-sweep "$RECOVERY_SWEEP_ID"}); CLAIM_RC=$?
|
|
@@ -472,31 +438,14 @@ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
|
472
438
|
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
473
439
|
ADAPTIVE_ENABLED=false
|
|
474
440
|
fi
|
|
475
|
-
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the
|
|
441
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the shared forge_resolve stub
|
|
476
442
|
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
477
443
|
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
478
|
-
# Resolution: $FORGEDOCK_HOME (authoritative when set) >
|
|
444
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > the running plugin root (FORGE_SOURCED_ROOT) > $FORGE_HOME > the
|
|
479
445
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
480
|
-
# FORGE_ROOT
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
484
|
-
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
485
|
-
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
486
|
-
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
487
|
-
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
488
|
-
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
489
|
-
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
490
|
-
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
491
|
-
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
492
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
493
|
-
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
494
|
-
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
495
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
496
|
-
while IFS= read -r _c; do
|
|
497
|
-
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
498
|
-
done <<< "$_k"
|
|
499
|
-
fi
|
|
446
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
447
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
448
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
500
449
|
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
501
450
|
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
502
451
|
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
@@ -769,6 +718,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {
|
|
|
769
718
|
| `re_gate_outcome: AUTO-LANDED` | Merged. Remediation's Phase M8 already ran `work-on:close`; if the issue is still open, run Phase 5 with `--terminal-state merged`. Then done. |
|
|
770
719
|
| `status: REREVIEW_REQUIRED` | Fallback only (should not occur at this depth): run the re-review from this router as in 0A.1 (labels checked and the per-head `FORGE:REREVIEW_DISPATCHED` claim posted and read back first, then `FORGE:REREVIEW_RELEASED` with `Outcome: SETTLED` posted only after the outcome is settled, forge#3486), then Phase 5 on `REVIEW_RESULT: status: COMPLETE`. If the re-review cannot run or returns anything else, apply the terminal fallback defined in 0A.1 (labels re-read first, comment with reason, add `needs-human`, remove `workflow:in-review`), then STOP; do not poll. |
|
|
771
720
|
| `status: BLOCKED`, blocker starts with `github-unavailable:` | Transient, not a decision (see Transient GitHub failures). Re-assert `workflow:remediating` with a short comment (remediation's Phase M0 only accepts `needs-human` or `workflow:remediating` issues and its M1 swaps the label for `workflow:in-review` on a FIXABLE run; a transient outage is not a human decision, so do not add `needs-human`), wait 2 min (5 min before the second retry), then re-invoke `work-on:remediate` with the same args; do NOT re-post the bound marker. At most 2 retries; if the third attempt still returns `github-unavailable:`, fall through to the `status: BLOCKED` (any kind) row below. |
|
|
721
|
+
| `status: BLOCKED`, blocker starts with `deferred-live-run:` | A live earlier remediation owns the PR (remediation's Phase M0 liveness step). Not a human decision: do NOT add `needs-human` and do NOT count it as an attempt. Make sure `workflow:remediating` is on the issue (re-add it if absent) and `needs-human` is not added by this row. STOP; the live run finishes, or its lease expires and a later invocation proceeds normally. |
|
|
772
722
|
| `status: BLOCKED` (any kind) | Make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with the blocker as a comment if absent: remediation can exit after its M1 cleared the label). STOP. |
|
|
773
723
|
| `status: ALREADY_DONE` (single-attempt guard: an earlier remediation already completed on this PR), `remediation: inpr-fix` | Same as any other `inpr-fix` outcome below: re-invoke Phase 4 once, which waives the in-PR gate. |
|
|
774
724
|
| `status: ALREADY_DONE`, `remediation: base-sync` | Make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with a comment naming the PR, that a base sync already ran and the conflicting files if known, if absent). STOP. Do NOT re-invoke Phase 4: a still-conflicting PR would loop. |
|
|
@@ -777,7 +727,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="{PR_NUMBER} --issue {
|
|
|
777
727
|
| any other outcome, `remediation: base-sync` | Terminal: make sure `needs-human` is on the issue and `workflow:remediating` is not (add/remove accordingly, with a comment naming the PR and the conflicting files from remediation's `FORGE:BASESYNC_FAILED` comment, if absent). STOP. Do NOT re-invoke Phase 4: the bound is used and a still-conflicting PR would loop. |
|
|
778
728
|
| any other outcome, `remediation: ci-gate` | Terminal: remediation left `needs-human` (or `workflow:awaiting-merge`) with its reason. STOP. |
|
|
779
729
|
|
|
780
|
-
Row order matters: the `github-unavailable:`
|
|
730
|
+
Row order matters: the `github-unavailable:` and `deferred-live-run:` rows are evaluated before the generic `status: BLOCKED` (any kind) row, which only handles non-transient blockers and the exhausted retry budget (see Transient GitHub failures). <!-- Added: forge#3402 -->
|
|
781
731
|
|
|
782
732
|
---
|
|
783
733
|
|
package/docs/CONFIG.md
CHANGED
|
@@ -500,9 +500,10 @@ build:
|
|
|
500
500
|
# straight to decompose without building. Default: 1.2. Skipped when threshold is 0.
|
|
501
501
|
estimate_factor: 1.2
|
|
502
502
|
|
|
503
|
-
# Pre-build estimate (investigate): a batch with at least this many
|
|
504
|
-
# seams
|
|
505
|
-
#
|
|
503
|
+
# Pre-build estimate (investigate): a non-batch issue with at least this many
|
|
504
|
+
# independent seams and an estimate over half the threshold is decomposed into
|
|
505
|
+
# packed sub-issues (not one per seam). Review-finding batches are exempt.
|
|
506
|
+
# Default: 3.
|
|
506
507
|
batch_seam_min: 3
|
|
507
508
|
|
|
508
509
|
# Extra exclusion globs, ADDITIVE to the built-in defaults below.
|
|
@@ -515,14 +516,16 @@ build:
|
|
|
515
516
|
|-------|------|----------|-------------|
|
|
516
517
|
| `diff_size.threshold` | integer | No | Changed lines allowed before the gate trips. Default: 1000. `0` disables the gate |
|
|
517
518
|
| `diff_size.estimate_factor` | number | No | Multiplier on `threshold` for the pre-build estimate: the investigator forces a decomposition when `ESTIMATED_DIFF_LINES` > `threshold` x `estimate_factor`. The investigator emits the effective value as an own-line `ESTIMATE_FACTOR` field, and the engine's fail-toward-decompose net (`isDecomposed`) uses it even if the verdict says NO (1.2 when the field is absent or invalid). That net is suppressed (treated as not decomposed) for a sub-issue (`**Parent**: #N`), an issue with a trusted `FORGE:DECOMPOSED` comment, or a justified `FORGE:SIZE_OVERRIDE`, because decompose Guard 1 refuses those. Default: 1.2. Skipped when `threshold` is `0` |
|
|
518
|
-
| `diff_size.batch_seam_min` | integer | No | Independent seams
|
|
519
|
+
| `diff_size.batch_seam_min` | integer | No | Independent seams at which an issue is decomposed when `ESTIMATED_DIFF_LINES` is over half of `threshold`. Does not apply to review-finding batches (`batch` label or `FORGE:BATCH_MEMBERS`), which decompose only above `threshold` x `estimate_factor`. Seams are packed into sub-issues, not one per seam. Default: 3. Skipped when `threshold` is `0` |
|
|
519
520
|
| `diff_size.exclude_globs` | list of strings | No | Globs excluded from the measurement, in addition to the built-in defaults. Default: `[]` |
|
|
520
521
|
|
|
521
522
|
**Built-in default exclusions** (always applied, defined once in `scripts/diff-size.sh`): lockfiles (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Cargo.lock`, `poetry.lock`, `uv.lock`, `go.sum`, `Gemfile.lock`, `composer.lock`), `*.min.*`, `*.snap`, and the directories `dist/`, `build/`, `generated/`, `fixtures/`, `vendor/` (anchored to the repo root, so first-party directories such as `commands/work-on/build/` are counted) plus `__generated__/` and `__snapshots__/` (any depth). The diff is measured against the merge base of `HEAD` and `origin/<base>`, so commits that landed on the base after the branch point are not counted. A configured glob ending in `/` matches that directory at any depth; any other glob is matched against the file basename and the full path. Globs are treated as data, never evaluated.
|
|
522
523
|
|
|
523
|
-
**Over the threshold**: the build posts a `FORGE:DIFF_SIZE` comment with a split proposal and exits `BUILD_RESULT: status: NEEDS_DECOMPOSE`; the router then runs `work-on:decompose`. To build the oversized diff anyway, post a `FORGE:SIZE_OVERRIDE` comment whose body has a non-empty justification on the line after the marker (only comments from trusted authors count). An issue that is already a decomposed child, or that already has `FORGE:DECOMPOSED`, is not decomposed again: the build blocks until an override is posted. The staged diff is also persisted before the exit (pushed ref `forge/wip/{NUMBER}`, or a patch file when the push fails) and each split-proposal line links its hunks, so they survive worktree cleanup; decompose copies the link into each sub-issue. The measured `diff_lines` is recorded in the `FORGE:TRAJECTORY` comment.
|
|
524
|
+
**Over the threshold**: the build posts a `FORGE:DIFF_SIZE` comment with a split proposal and exits `BUILD_RESULT: status: NEEDS_DECOMPOSE`; the router then runs `work-on:decompose`. To build the oversized diff anyway, post a `FORGE:SIZE_OVERRIDE` comment whose body has a non-empty justification on the line after the marker (only comments from trusted authors count). An issue that is already a decomposed child, or that already has `FORGE:DECOMPOSED`, is not decomposed again: the build blocks until an override is posted, unless the diff is cohesive (next paragraph). The staged diff is also persisted before the exit (pushed ref `forge/wip/{NUMBER}`, or a patch file when the push fails) and each split-proposal line links its hunks, so they survive worktree cleanup; decompose copies the link into each sub-issue. The measured `diff_lines` is recorded in the `FORGE:TRAJECTORY` comment.
|
|
524
525
|
|
|
525
|
-
**
|
|
526
|
+
**Cohesive sub-issue overage**: a sub-issue (or an issue with `FORGE:DECOMPOSED`) that is over the threshold with no override continues instead of blocking when `scripts/diff-size.sh` reports `cohesive=true`: at least 70% of the counted lines are in files added by the diff or in test files (test directories `test/`, `tests/`, `__tests__/`, or `*.test.*`, `*.spec.*`, `test_*`, `*_test.*`), and `diff_lines` is at most `threshold` x 1.5. The build records `result: COHESIVE` with the cohesion figures and a reviewer note in `FORGE:DIFF_SIZE`, adds no `needs-human`, and continues to validation. Test lines are classified, not discounted. The 70% and 1.5x constants live in the script and are not configurable. A non-sub-issue over the threshold still routes to `NEEDS_DECOMPOSE`, and `FORGE:SIZE_OVERRIDE` still takes precedence. Each sub-issue is packed to at most `0.8 x threshold` and carries an own-line `ESTIMATED_DIFF_LINES: N` in its body, which `FORGE:DIFF_SIZE` records next to the actual `diff_lines` as `estimated_diff_lines`.
|
|
527
|
+
|
|
528
|
+
**Pre-build estimate**: `work-on:investigate` emits `ESTIMATED_DIFF_LINES`, `INDEPENDENT_SEAMS`, `GATE_THRESHOLD` and `ESTIMATE_FACTOR` inside `### Decomposition Assessment`. An estimate above `threshold` x `estimate_factor` (or, for a non-batch issue, `batch_seam_min`+ seams whose estimate is over `threshold` / 2) answers `**YES**`, so the issue is decomposed before any build into `max(2, ceil(estimate / (0.8 x threshold)))` packed sub-issues (the parent is a terminal tracker). The engine downgrades a `**YES**` that neither condition requires to NO unless the report has an own-line `DECOMPOSE_JUSTIFICATION:` line (it assumes `batch_seam_min` 3 and cannot see the `batch` label, so it is lenient for batches). The estimate rule is skipped when `threshold` is `0`.
|
|
526
529
|
|
|
527
530
|
**Commands that use this section**: `work-on` (build phase B5.5, `decompose`, `close`)
|
|
528
531
|
|
|
@@ -939,7 +942,7 @@ These variables are read from the operator's runner environment, not from `forge
|
|
|
939
942
|
|
|
940
943
|
### Install root resolution
|
|
941
944
|
|
|
942
|
-
`FORGE_ROOT` (where the merge trail gate, dispatch lint and engine modules are resolved) is computed in this order: `FORGEDOCK_HOME` (authoritative: when set, nothing else is consulted, and a relative value leaves `FORGE_ROOT` empty so the gate fails closed) > the running plugin's own root (substituted by Claude Code
|
|
945
|
+
`FORGE_ROOT` (where the merge trail gate, dispatch lint and engine modules are resolved) is computed in this order: `FORGEDOCK_HOME` (authoritative: when set, nothing else is consulted, and a relative value leaves `FORGE_ROOT` empty so the gate fails closed) > the running plugin's own root (substituted by Claude Code, not a runtime env var; the stub hands it to the helper as `FORGE_SOURCED_ROOT`, so it wins over `FORGE_ROOT`, `FORGE_HOME` and any newer cached version) > `FORGE_HOME` > the `~/.claude/commands/work-on.md` symlink target > the clone path recorded in `$CODEX_HOME/forge-home` (Codex; a relative `CODEX_HOME` is ignored) > the newest semver directory in the Claude plugin cache (official `forgedock` marketplace only; `FORGEDOCK_MARKETPLACE` pins a differently named one) > the marketplace directory. If nothing resolves, `FORGE_ROOT` stays empty and the trail gate refuses to merge (fail closed). See [CODEX.md](CODEX.md#forge_home-and-install-root-resolution).
|
|
943
946
|
|
|
944
947
|
### Keeping the phase-trail merge gate satisfiable
|
|
945
948
|
|
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Operator-facing notes for behavior changes that need action or awareness. Newest first.
|
|
4
4
|
|
|
5
|
+
## remediate.md uses the shared resolver stub (#3779)
|
|
6
|
+
|
|
7
|
+
`commands/work-on/remediate.md` no longer inlines the `FORGE_ROOT` bootstrap or the `TRUSTED_SCRIPT` / `CI_GATE_SCRIPT` resolvers; each fence uses the pinned `forge_resolve` locator from `scripts/lib/forge-resolve.sh` (unresolved script => empty => the existing strict branch). The spec shrinks from 95,283 to 80,957 bytes (-15.0%).
|
|
8
|
+
|
|
9
|
+
Action: none. Internal refactor; resolution tiers and fail-closed behavior are unchanged.
|
|
10
|
+
|
|
5
11
|
## Re-flag gate ignores generic words (#3719)
|
|
6
12
|
|
|
7
13
|
`scripts/classify-finding.sh` no longer counts generic stopwords (`the`, `with`, `before`, `write`, ...) as topical similarity in the cross-head/cross-PR re-flag gate. The line-overlap match needs >= 1 shared non-stopword title token, and the title-only match needs >= 3 shared tokens of which >= 2 are non-stopword. Domain words such as `lock` and `retry` still count. `/review-pr` §6B.5 and `/review-pr-staging` §7B.5 carry the same wording.
|
package/docs/WORK-ON-RUNTIME.md
CHANGED
|
@@ -108,7 +108,7 @@ remediate L2, reviewers L3, same as the table.
|
|
|
108
108
|
|
|
109
109
|
| Phase result | Router action |
|
|
110
110
|
|--------------|---------------|
|
|
111
|
-
| `REVIEW_RESULT: status: NEXT, next: remediate` | Phase 4R: post the bound marker (`CI_REMEDIATION` / `INPR_REMEDIATION` / `BASESYNC_REMEDIATION`, the last for `remediation: base-sync`, a merge-only base sync) immediately before invoking `work-on:remediate`, so an interrupted handoff is retried, not counted. `AUTO-LANDED` → done (remediation already ran close). `REREVIEW_REQUIRED` → fallback re-review from the router. `ALREADY_DONE` (single-attempt guard) → `inpr-fix`: re-invoke review (waive); `ci-gate`: ensure `needs-human`, stop. `BLOCKED` → ensure `needs-human`, stop. Resume after an interrupted remediation (no `needs-human`, no later `REMEDIATION:COMPLETE`) re-adds `needs-human` and returns to 4R, not to review. Other outcome: `inpr-fix` → re-invoke review once (it waives the in-PR gate); `ci-gate` → terminal. |
|
|
111
|
+
| `REVIEW_RESULT: status: NEXT, next: remediate` | Phase 4R: post the bound marker (`CI_REMEDIATION` / `INPR_REMEDIATION` / `BASESYNC_REMEDIATION`, the last for `remediation: base-sync`, a merge-only base sync) immediately before invoking `work-on:remediate`, so an interrupted handoff is retried, not counted. `AUTO-LANDED` → done (remediation already ran close). `REREVIEW_REQUIRED` → fallback re-review from the router. `ALREADY_DONE` (single-attempt guard) → `inpr-fix`: re-invoke review (waive); `ci-gate`: ensure `needs-human`, stop. `BLOCKED` with a `deferred-live-run:` blocker (a live earlier remediation owns the PR) → no `needs-human`, keep `workflow:remediating`, stop. Any other `BLOCKED` → ensure `needs-human`, stop. Resume after an interrupted remediation (no `needs-human`, no later `REMEDIATION:COMPLETE`) re-adds `needs-human` and returns to 4R, not to review. Other outcome: `inpr-fix` → re-invoke review once (it waives the in-PR gate); `ci-gate` → terminal. |
|
|
112
112
|
| `REMEDIATE_RESULT` from `--remediate` entry | unchanged (Phase 0A.1) |
|
|
113
113
|
|
|
114
114
|
## 6. What does not change
|
|
@@ -535,7 +535,7 @@ needed.
|
|
|
535
535
|
|
|
536
536
|
| Marker | Category | Meaning |
|
|
537
537
|
|--------|----------|---------|
|
|
538
|
-
| `<!-- FORGE:DIFF_SIZE -->` | Lifecycle (no completion sentinel) | Measured diff size of the staged build: `diff_lines`, `excluded_lines`, `threshold`, `result: OK \| OVERRIDDEN \| OVER`, and on `OVER` a `### Split Proposal`. Refreshed in place; readers take the latest trusted comment. Consumed by decompose (plan source) and close (records `diff_lines` in `FORGE:TRAJECTORY`) |
|
|
538
|
+
| `<!-- FORGE:DIFF_SIZE -->` | Lifecycle (no completion sentinel) | Measured diff size of the staged build: `diff_lines`, `excluded_lines`, `threshold`, `result: OK \| OVERRIDDEN \| COHESIVE \| OVER`, optional `estimated_diff_lines` (sub-issues), cohesion figures and a reviewer note on `COHESIVE`, and on `OVER` a `### Split Proposal`. Refreshed in place; readers take the latest trusted comment. Consumed by decompose (plan source) and close (records `diff_lines` in `FORGE:TRAJECTORY`) |
|
|
539
539
|
| `<!-- FORGE:SIZE_OVERRIDE -->` | Control | Posted by a human to let an over-threshold build proceed. Valid only from a trusted author and with a non-empty justification on the line after the marker |
|
|
540
540
|
|
|
541
541
|
Both lookups are anchored at the start of the comment body and trust-filtered
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.2",
|
|
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"
|
|
@@ -75,6 +75,7 @@ fi
|
|
|
75
75
|
# ---------------------------------------------------------------------------
|
|
76
76
|
MARKER_REGISTRY="
|
|
77
77
|
ACCEPTANCE_GATE
|
|
78
|
+
ACCEPTANCE_GATE_REVIEW
|
|
78
79
|
ADR_EXTRACTED
|
|
79
80
|
ANCESTRY_FAILED
|
|
80
81
|
ARCHITECT
|
|
@@ -163,6 +164,7 @@ MARKER_REGISTRY="
|
|
|
163
164
|
RECOVERY_CLAIM_RELEASED
|
|
164
165
|
REMEDIATION
|
|
165
166
|
REMEDIATION_HELD
|
|
167
|
+
REMEDIATION_DEFERRED
|
|
166
168
|
REREVIEW_DISPATCHED
|
|
167
169
|
REREVIEW_RELEASED
|
|
168
170
|
REREVIEW_SKIPPED
|
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Usage: check-spec-size.sh [base_ref] (default: $BASE_REF, else origin/staging)
|
|
7
7
|
# Env: PR_LABELS comma-separated PR label names (untrusted; grep-matched only)
|
|
8
|
-
#
|
|
8
|
+
# PR_HEAD_REF / PR_BASE_REF PR head and base branch names; staging -> main
|
|
9
|
+
# (a release PR) is skipped: cumulative growth is not judged here
|
|
10
|
+
# PR_SAME_REPO must be "true" for the release skip (a fork branch named
|
|
11
|
+
# staging must not bypass the budget)
|
|
9
12
|
# GITHUB_STEP_SUMMARY summary file (stdout when unset)
|
|
10
13
|
# Exit: 0 pass/exempt, 1 budget violation, 2 usage/base-ref error (fail closed)
|
|
11
14
|
#
|
|
12
|
-
# Rules (against the PR base, compared in bytes):
|
|
15
|
+
# Rules (against the merge-base of HEAD and the PR base, compared in bytes):
|
|
13
16
|
# 1. Budgeted specs may not grow by more than 2% or more than 4096 bytes.
|
|
14
|
-
# 2.
|
|
15
|
-
#
|
|
17
|
+
# 2. The NET count of canonical block markers across all of commands/**/*.md
|
|
18
|
+
# may not rise (moving a block between files, or replacing one with a stub,
|
|
19
|
+
# is allowed). Count-based until the shared helper lands (#3493).
|
|
16
20
|
# Shrinking or equal size always passes.
|
|
17
|
-
# Exemption: `spec-size-exempt` label
|
|
18
|
-
#
|
|
21
|
+
# Exemption: the `spec-size-exempt` label only (needs triage rights; a PR-body
|
|
22
|
+
# marker is NOT honoured). Failures are reported, exit 0.
|
|
19
23
|
# Bash 3.2 portable: no assoc arrays, mapfile, grep -P.
|
|
20
24
|
set -uo pipefail
|
|
21
25
|
|
|
@@ -32,15 +36,27 @@ if ! git rev-parse --verify -q "$BASE^{commit}" >/dev/null; then
|
|
|
32
36
|
exit 2
|
|
33
37
|
fi
|
|
34
38
|
|
|
35
|
-
# ---
|
|
39
|
+
# --- release PR skip (staging -> main) ---------------------------------------
|
|
40
|
+
if [ "${PR_HEAD_REF:-}" = "staging" ] && [ "${PR_BASE_REF:-}" = "main" ] && [ "${PR_SAME_REPO:-}" = "true" ]; then
|
|
41
|
+
out "## Spec-size budget"
|
|
42
|
+
out ""
|
|
43
|
+
out "Result: SKIPPED (release PR staging -> main; growth was checked per PR)"
|
|
44
|
+
echo "spec-size: skipped for release PR staging -> main"
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Compare against the merge-base, not the base tip (base moves after branching).
|
|
49
|
+
MB=$(git merge-base HEAD "$BASE" 2>/dev/null) || MB=""
|
|
50
|
+
if [ -z "$MB" ]; then
|
|
51
|
+
echo "ERROR: no merge-base between HEAD and '$BASE' (need fetch-depth: 0)" >&2
|
|
52
|
+
exit 2
|
|
53
|
+
fi
|
|
54
|
+
BASE="$MB"
|
|
55
|
+
|
|
56
|
+
# --- exemption: label only ---------------------------------------------------
|
|
36
57
|
EXEMPT_REASON=""
|
|
37
58
|
if printf '%s' "${PR_LABELS:-}" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -qxF 'spec-size-exempt'; then
|
|
38
59
|
EXEMPT_REASON="label spec-size-exempt"
|
|
39
|
-
else
|
|
40
|
-
R=$(printf '%s\n' "${PR_BODY:-}" | tr -d '\r' \
|
|
41
|
-
| sed -n 's/^[[:space:]]*<!--[[:space:]]*spec-size-exempt:[[:space:]]*\(.*\)-->[[:space:]]*$/\1/p' \
|
|
42
|
-
| head -n 1 | sed 's/[[:space:]]*$//')
|
|
43
|
-
[ -n "$R" ] && EXEMPT_REASON="PR body marker: $R"
|
|
44
60
|
fi
|
|
45
61
|
|
|
46
62
|
FAILURES=0
|
|
@@ -66,25 +82,29 @@ for f in $BUDGETED; do
|
|
|
66
82
|
out "| \`$f\` | $b | $h | $g | $p | $status |"
|
|
67
83
|
done
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
# Net marker total across every commands/**/*.md: head (working tree, incl. new
|
|
86
|
+
# files) vs base (incl. files deleted at head). Fail only when the total rises.
|
|
87
|
+
HEAD_TOTAL=0; BASE_TOTAL=0; MARKER_ROWS=""
|
|
88
|
+
HEAD_FILES=$(git ls-files 'commands/*.md' | sort -u)
|
|
89
|
+
BASE_FILES=$(git ls-tree -r --name-only "$BASE" -- commands | grep -E '\.md$' | sort -u)
|
|
90
|
+
for f in $(printf '%s\n%s\n' "$HEAD_FILES" "$BASE_FILES" | sort -u); do
|
|
91
|
+
[ -n "$f" ] || continue
|
|
92
|
+
hc=0; [ -f "$f" ] && hc=$(grep -cE "$MARKERS" "$f" 2>/dev/null || true); hc=${hc:-0}
|
|
74
93
|
bc=$(git show "$BASE:$f" 2>/dev/null | grep -cE "$MARKERS" 2>/dev/null || true); bc=${bc:-0}
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
HEAD_TOTAL=$((HEAD_TOTAL + hc)); BASE_TOTAL=$((BASE_TOTAL + bc))
|
|
95
|
+
[ "$hc" -ne "$bc" ] && MARKER_ROWS="$MARKER_ROWS| \`$f\` | $bc | $hc |
|
|
77
96
|
"
|
|
78
|
-
FAILURES=$((FAILURES + 1))
|
|
79
|
-
fi
|
|
80
97
|
done
|
|
81
|
-
if [ -
|
|
98
|
+
if [ "$HEAD_TOTAL" -gt "$BASE_TOTAL" ]; then
|
|
99
|
+
FAILURES=$((FAILURES + 1))
|
|
82
100
|
out ""
|
|
83
|
-
out "###
|
|
101
|
+
out "### NEW CANONICAL BLOCK COPY: net marker total rose $BASE_TOTAL -> $HEAD_TOTAL (source the shared helper instead)"
|
|
102
|
+
fi
|
|
103
|
+
if [ -n "$MARKER_ROWS" ]; then
|
|
84
104
|
out ""
|
|
85
|
-
out "| File | Base markers | Head markers |
|
|
86
|
-
out "
|
|
87
|
-
printf '%s' "$
|
|
105
|
+
out "| File | Base markers | Head markers |"
|
|
106
|
+
out "|------|-----:|-----:|"
|
|
107
|
+
printf '%s' "$MARKER_ROWS" >> "$SUMMARY"
|
|
88
108
|
fi
|
|
89
109
|
|
|
90
110
|
out ""
|
|
@@ -97,6 +117,6 @@ if [ -n "$EXEMPT_REASON" ]; then
|
|
|
97
117
|
echo "spec-size: $FAILURES violation(s) waived: $EXEMPT_REASON"
|
|
98
118
|
exit 0
|
|
99
119
|
fi
|
|
100
|
-
out "Result: FAIL ($FAILURES violation(s)). Shrink the spec, or
|
|
120
|
+
out "Result: FAIL ($FAILURES violation(s)). Shrink the spec, or ask a maintainer to add the spec-size-exempt label."
|
|
101
121
|
echo "spec-size: FAIL ($FAILURES violation(s)); see job summary" >&2
|
|
102
122
|
exit 1
|
|
@@ -18,6 +18,7 @@ gen() { awk -v n="$1" 'BEGIN{for(i=0;i<n;i++) print "line of spec text number "
|
|
|
18
18
|
gen 4000 > commands/review-pr.md # ~110 KB: 2% ~ 2.2 KB < 4 KB cap
|
|
19
19
|
gen 20000 > commands/work-on.md # ~560 KB: 2% ~ 11 KB, so the 4 KB cap binds
|
|
20
20
|
gen 100 > commands/other.md
|
|
21
|
+
printf 'HELPER_SCRIPT=""\n' > commands/donor.md
|
|
21
22
|
gen 100 > commands/work-on/remediate.md
|
|
22
23
|
git add -A; git commit -qm base; git branch -M staging; git push -q origin staging 2>/dev/null
|
|
23
24
|
git checkout -q -b feat
|
|
@@ -26,7 +27,7 @@ reset() { git reset -q --hard origin/staging; git clean -qfd; }
|
|
|
26
27
|
run() { # name want_rc [env...]
|
|
27
28
|
local name="$1" want="$2"; shift 2
|
|
28
29
|
local sum="$TMP/summary"; : > "$sum"
|
|
29
|
-
env GITHUB_STEP_SUMMARY="$sum" PR_LABELS=""
|
|
30
|
+
env GITHUB_STEP_SUMMARY="$sum" PR_LABELS="" PR_HEAD_REF="" PR_BASE_REF="" "$@" bash "$S" origin/staging >"$TMP/out" 2>"$TMP/err"
|
|
30
31
|
local rc=$?
|
|
31
32
|
if [ "$rc" = "$want" ]; then ok; else bad "$name (rc=$rc want $want)"; cat "$TMP/err"; fi
|
|
32
33
|
}
|
|
@@ -72,10 +73,52 @@ run "reduced count passes" 0
|
|
|
72
73
|
# Exemption
|
|
73
74
|
reset; gen 100 >> commands/review-pr.md
|
|
74
75
|
run "exempt by label" 0 PR_LABELS="bug,spec-size-exempt"; has "label reported" "EXEMPT"
|
|
75
|
-
run "
|
|
76
|
-
run "empty-reason marker not exempt" 1 PR_BODY='<!-- spec-size-exempt: -->'
|
|
76
|
+
run "body marker does not exempt" 1 PR_BODY=$'text\n<!-- spec-size-exempt: security fix -->\nmore'
|
|
77
77
|
run "label substring not exempt" 1 PR_LABELS="spec-size-exempt-not"
|
|
78
78
|
|
|
79
|
+
# Net counting: moves and stubs pass (the #3493 / #3590 shape)
|
|
80
|
+
reset; : > commands/donor.md; printf 'HELPER_SCRIPT=""\n' >> commands/other.md
|
|
81
|
+
run "net move between files passes" 0
|
|
82
|
+
reset; : > commands/donor.md; printf 'HELPER_SCRIPT=""\n' > commands/new-spec.md; git add -A
|
|
83
|
+
run "move to a new file passes" 0
|
|
84
|
+
reset; git rm -q commands/donor.md; gen 5 > commands/review-pr.md; printf 'HELPER_SCRIPT=""\n' > commands/helper-stub.md; git add -A
|
|
85
|
+
run "stub replaces deleted block passes" 0
|
|
86
|
+
reset; printf 'HELPER_SCRIPT=""\n' >> commands/other.md
|
|
87
|
+
run "copy without removal still fails" 1
|
|
88
|
+
|
|
89
|
+
# Shared-helper stubs (forge#3778): the sourced-helper stub shape carries none of the markers, so converting an inlined block to a stub
|
|
90
|
+
# is a net reduction, while re-pasting the full block still fails.
|
|
91
|
+
STUB_LOC='type forge_resolve >/dev/null 2>&1 || for _r in "${FORGEDOCK_HOME:-}"; do . "$_r/scripts/lib/forge-resolve.sh"; done'
|
|
92
|
+
reset; printf '%s\n' "$STUB_LOC" '. "$(forge_resolve lib/fence-bootstrap.sh)" || exit 1 # FENCE_BOOTSTRAP' 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/other.md
|
|
93
|
+
run "one-tag-per-stub design passes" 0
|
|
94
|
+
reset; : > commands/donor.md; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/other.md
|
|
95
|
+
run "block converted to a stub passes" 0
|
|
96
|
+
reset; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=""' >> commands/other.md
|
|
97
|
+
run "stub plus a re-pasted resolver block fails" 1
|
|
98
|
+
|
|
99
|
+
# Release PR skip (staging -> main only)
|
|
100
|
+
reset; gen 100 >> commands/review-pr.md
|
|
101
|
+
run "release PR skipped" 0 PR_HEAD_REF=staging PR_BASE_REF=main PR_SAME_REPO=true; has "skip reported" "SKIPPED"
|
|
102
|
+
run "fork branch named staging still checked" 1 PR_HEAD_REF=staging PR_BASE_REF=main PR_SAME_REPO=false
|
|
103
|
+
run "release PR without same-repo flag still checked" 1 PR_HEAD_REF=staging PR_BASE_REF=main
|
|
104
|
+
run "hotfix to main still checked" 1 PR_HEAD_REF=hotfix PR_BASE_REF=main
|
|
105
|
+
run "staging to other base still checked" 1 PR_HEAD_REF=staging PR_BASE_REF=dev
|
|
106
|
+
|
|
107
|
+
# Merge-base: base advancing after branching must not count against the PR
|
|
108
|
+
reset; git checkout -q -b old-branch; git checkout -q staging 2>/dev/null || git checkout -q -B staging origin/staging
|
|
109
|
+
gen 300 >> commands/review-pr.md; git commit -qam "base grows"; git push -q origin HEAD:staging 2>/dev/null
|
|
110
|
+
git checkout -q old-branch; git fetch -q origin staging
|
|
111
|
+
run "base tip growth ignored (merge-base)" 0
|
|
112
|
+
git checkout -q feat; git branch -qD old-branch; git branch -qf staging origin/staging; reset
|
|
113
|
+
|
|
114
|
+
# Script extracted from the base ref behaves the same
|
|
115
|
+
git show origin/staging:scripts/check-spec-size.sh > "$TMP/base-script.sh" 2>/dev/null || cp "$S" "$TMP/base-script.sh"
|
|
116
|
+
gen 100 >> commands/review-pr.md
|
|
117
|
+
: > "$TMP/summary"
|
|
118
|
+
GITHUB_STEP_SUMMARY="$TMP/summary" PR_LABELS="" bash "$TMP/base-script.sh" origin/staging >/dev/null 2>&1; rc=$?
|
|
119
|
+
[ "$rc" = 1 ] && ok || bad "extracted base-ref script fails over-budget change (rc=$rc)"
|
|
120
|
+
reset
|
|
121
|
+
|
|
79
122
|
# Fail closed on unresolvable base
|
|
80
123
|
: > "$TMP/summary"
|
|
81
124
|
GITHUB_STEP_SUMMARY="$TMP/summary" bash "$S" origin/does-not-exist >/dev/null 2>&1; rc=$?
|