forgedock 1.8.4 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/admission.mjs +23 -4
- package/bin/engine/admission.test.mjs +37 -0
- package/commands/orchestrate/phase-1-resolve.md +40 -24
- package/commands/orchestrate/phase-3-dependency.md +56 -11
- package/commands/orchestrate/phase-4-execution.md +359 -111
- package/commands/orchestrate/phase-5-cleanup.md +17 -0
- package/commands/orchestrate/phase-6-report.md +1 -1
- package/commands/quality-gate.md +80 -0
- package/commands/recover-orphans.md +3 -3
- package/commands/review-pr.md +98 -16
- package/commands/work-on/build/context.md +25 -4
- package/commands/work-on/build/validate.md +16 -9
- package/commands/work-on/build.md +26 -9
- package/commands/work-on/close.md +104 -40
- package/commands/work-on/decompose.md +13 -7
- package/commands/work-on/investigate.md +13 -7
- package/commands/work-on/remediate.md +15 -4
- package/commands/work-on/review.md +48 -19
- package/commands/work-on.md +62 -9
- package/docs/CODEX.md +12 -0
- package/docs/CONFIG.md +49 -1
- package/docs/RELEASE-NOTES.md +61 -0
- package/package.json +1 -1
- package/scripts/amplification-breaker.sh +89 -0
- package/scripts/amplification-breaker.test.sh +39 -0
- package/scripts/check-admission-parity.mjs +74 -1
- package/scripts/check-spec-bash.sh +120 -0
- package/scripts/check-spec-bash.test.sh +90 -0
- package/scripts/check-spec-markers.sh +67 -3
- package/scripts/classify-finding.sh +123 -0
- package/scripts/classify-finding.test.sh +77 -0
- package/scripts/close-dossier-no-main-commit.test.sh +139 -0
- package/scripts/forge-root.test.sh +84 -27
- package/scripts/lint-build-validate-files.sh +29 -0
- package/scripts/lint-build-validate-files.test.sh +25 -0
- package/scripts/lint-dispatch-prompt.test.sh +2 -1
- package/scripts/recovery-claim-live.sh +73 -0
- package/scripts/recovery-claim-live.test.sh +66 -0
- package/scripts/verify-phase-trail.sh +183 -4
- package/scripts/verify-phase-trail.test.sh +211 -1
|
@@ -24,6 +24,8 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
24
24
|
|
|
25
25
|
**CRITICAL: You MUST execute ALL phases B0–B6.5 in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
26
26
|
|
|
27
|
+
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
28
|
+
|
|
27
29
|
<!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
|
|
28
30
|
|
|
29
31
|
---
|
|
@@ -69,14 +71,20 @@ fi
|
|
|
69
71
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
70
72
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
71
73
|
FORGE_ROOT=""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
75
|
+
_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
|
|
76
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
77
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
78
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
79
|
+
# 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.
|
|
80
|
+
_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")")"
|
|
81
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
82
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
83
|
+
# 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)
|
|
84
|
+
_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)"
|
|
85
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
78
86
|
# '${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.
|
|
79
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
87
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
80
88
|
while IFS= read -r _c; do
|
|
81
89
|
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
|
|
82
90
|
done <<< "$_k"
|
|
@@ -466,6 +474,7 @@ If `FUNCTION_NAMES` is empty, omit `--functions`. The child posts its own `FORGE
|
|
|
466
474
|
- `status: COMPLETE | PARTIAL | SKIPPED` → continue to B4
|
|
467
475
|
- No `CONTEXT_RESULT:` block, timeout or error → log a warning and continue to B4 (context is advisory and non-blocking)
|
|
468
476
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:context"
|
|
477
|
+
- Returned running/backgrounded/empty (no `CONTEXT_RESULT:`) → do not end the turn; re-read for a `FORGE:CONTEXT` marker, re-invoke the same child if absent, per the bounded re-invoke rule (at most 2; check the marker before re-invoking) (advisory rules above still apply once it returns)
|
|
469
478
|
# MUST CONTINUE to Phase B4 — context result is intermediate, NOT terminal.
|
|
470
479
|
|
|
471
480
|
---
|
|
@@ -485,6 +494,7 @@ The child posts its own `FORGE:ARCHITECT` comment (a "Skipped" marker + `:COMPLE
|
|
|
485
494
|
- `status: BLOCKED` (conflicting constraints that cannot be resolved) → Blocked exit with the child's `blocker`
|
|
486
495
|
- No `ARCHITECT_RESULT:` block → re-read the issue: if a `FORGE:ARCHITECT:COMPLETE` marker exists continue to B5, otherwise Blocked exit "architect produced no result"
|
|
487
496
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:architect"
|
|
497
|
+
- Returned running/backgrounded/empty → do not end the turn to wait; re-read markers and re-invoke the same child per the bounded re-invoke rule (at most 2; the marker fallback above applies only after a real return)
|
|
488
498
|
# MUST CONTINUE to Phase B5 — architect result is intermediate, NOT terminal.
|
|
489
499
|
|
|
490
500
|
---
|
|
@@ -503,6 +513,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
503
513
|
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B6/B6.5)
|
|
504
514
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
505
515
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
516
|
+
- Returned running/backgrounded/empty (no `IMPLEMENT_RESULT:`) → do not end the turn to wait; re-read the `FORGE:BUILDER` comment and the worktree state; if `FORGE:BUILDER:COMPLETE` is present consume it, and if the worktree shows in-flight edits or a fresh commit treat the child as possibly live; before re-invoking, apply the bounded re-invoke rule (at most 2, then `child-stalled: implement`)
|
|
506
517
|
# MUST CONTINUE to Phase B6 — implement result is intermediate, NOT terminal (validation still required).
|
|
507
518
|
|
|
508
519
|
---
|
|
@@ -522,9 +533,10 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
522
533
|
```
|
|
523
534
|
|
|
524
535
|
**After subcommand returns** (read its `VALIDATE_RESULT:` block):
|
|
525
|
-
- `gate_passed: true` → verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-invoke validate once; a missing marker is not a pass. Then continue to Phase B6.5 (acceptance gate)
|
|
536
|
+
- `gate_passed: true` → verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-derive `{CHANGED_FILES}` with the B6 command above and re-invoke validate once with the full B6 `Skill(...)` call, including `--files "{CHANGED_FILES}"` (never omit `--files`; an empty list falls back to the git diff); a missing marker is not a pass. Then continue to Phase B6.5 (acceptance gate)
|
|
526
537
|
- `gate_passed: false` → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted)
|
|
527
538
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
|
|
539
|
+
- Returned running/backgrounded/empty (no `VALIDATE_RESULT:`) → do not end the turn to wait; re-read the `FORGE:QUALITY_GATE` marker (consume it if present) and the worktree state before re-invoking, then re-invoke the same child under the bounded re-invoke rule (at most 2, then `child-stalled: validate`); this cap is separate from the single marker-absent re-invoke above
|
|
528
540
|
|
|
529
541
|
---
|
|
530
542
|
|
|
@@ -683,7 +695,12 @@ If `GATE_PASS = false`, the build repairs itself **once** before anything escala
|
|
|
683
695
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
684
696
|
2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
|
|
685
697
|
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
|
|
686
|
-
|
|
698
|
+
then re-derive `{CHANGED_FILES}` from the repaired worktree (never reuse the pre-repair value, and never omit `--files`; an empty list falls back to this diff):
|
|
699
|
+
```bash
|
|
700
|
+
CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HEAD" 2>/dev/null | tr '\n' ' ' | xargs)
|
|
701
|
+
```
|
|
702
|
+
and invoke validate explicitly (the new code gets a fresh quality gate and commit):
|
|
703
|
+
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:validate", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --files \"{CHANGED_FILES}\"")`
|
|
687
704
|
3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
|
|
688
705
|
4. Still failing (or already repaired once) → leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed after one repair — see FORGE:ACCEPTANCE_GATE comment".
|
|
689
706
|
|
|
@@ -101,14 +101,20 @@ fi
|
|
|
101
101
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
102
102
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
103
103
|
FORGE_ROOT=""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
105
|
+
_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
|
|
106
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
107
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
108
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
109
|
+
# 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.
|
|
110
|
+
_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")")"
|
|
111
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
112
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
113
|
+
# 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)
|
|
114
|
+
_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)"
|
|
115
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
110
116
|
# '${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.
|
|
111
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
117
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
112
118
|
while IFS= read -r _c; do
|
|
113
119
|
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
|
|
114
120
|
done <<< "$_k"
|
|
@@ -422,11 +428,25 @@ The `REMAINING_AFTER` variable is passed to Phase C2 to decide whether to close.
|
|
|
422
428
|
|
|
423
429
|
**This phase is non-blocking** — if the dossier write fails, log the reason and continue to Phase C1.5. Never stall close for dossier maintenance.
|
|
424
430
|
|
|
425
|
-
**
|
|
431
|
+
**Never touch the main checkout**: `$REPO_PATH` is the operator's main checkout. This phase never writes, stages or commits there (no git add or git commit against it, no directory change into it). `index.yaml` is only read from it. The dossier edit is made in a temporary detached worktree off `origin/${DOSSIER_BASE}`, committed on branch `docs/dossier-{NUMBER}`, pushed, and opened as a PR (reviewed path to the base branch); the temporary worktree is always removed (EXIT trap). Every failure logs `Phase C1.7: skipped - <reason>` and continues.
|
|
426
432
|
|
|
427
|
-
|
|
433
|
+
**Skip if**: `{PR_NUMBER}` is empty (investigation / decomposed / invalid terminals — `TERMINAL_STATE` is not `merged`) OR `$REPO_PATH` is unset OR `devdocs/index.yaml` does not contain a `modules:` section OR no PR files match any module glob OR no `origin` remote / no push rights / worktree creation fails / PR creation fails (each logged as `Phase C1.7: skipped - <reason>`).
|
|
434
|
+
|
|
435
|
+
### Steps 1-3: One self-contained block
|
|
436
|
+
|
|
437
|
+
**Single block, by design**: shell state (variables, traps) does not persist between Bash tool calls, so Steps 1-3 below run as ONE fenced block in ONE Bash call. Splitting it would leave `DOSSIER_TMP`, `INDEX_PATH`, `DOSSIER_UPDATED_MODULES` and `CHANGED_FILES_RAW` empty in later calls and leak the temporary worktree. An `EXIT` trap removes the worktree and prunes on any exit path (skip, abort, interrupt); each skip is a `return` from `_dossier_run`, which continues to Phase C1.5.
|
|
428
438
|
|
|
429
439
|
```bash
|
|
440
|
+
_dossier_cleanup() {
|
|
441
|
+
if [ -n "${DOSSIER_TMP:-}" ]; then
|
|
442
|
+
git -C "${REPO_PATH}" worktree remove --force "$DOSSIER_TMP" >/dev/null 2>&1 || rm -rf "$DOSSIER_TMP"
|
|
443
|
+
git -C "${REPO_PATH}" worktree prune >/dev/null 2>&1 || true
|
|
444
|
+
DOSSIER_TMP=""
|
|
445
|
+
fi
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_dossier_run() {
|
|
449
|
+
# --- Step 1: Resolve affected files from FORGE:BUILDER comment ---
|
|
430
450
|
# Read FORGE:BUILDER comment to get the list of changed files
|
|
431
451
|
BUILDER_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
432
452
|
--jq '[.[] | select(.body | contains("FORGE:BUILDER"))] | last | .body // ""' 2>/dev/null || echo "")
|
|
@@ -452,21 +472,18 @@ fi
|
|
|
452
472
|
|
|
453
473
|
if [ -z "$CHANGED_FILES_RAW" ]; then
|
|
454
474
|
echo "Phase C1.7: No changed files found from FORGE:BUILDER or FORGE:INVESTIGATOR — skipping dossier append"
|
|
455
|
-
|
|
475
|
+
return 0
|
|
456
476
|
fi
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Step 2: Match against module globs and append entries
|
|
460
477
|
|
|
461
|
-
|
|
478
|
+
# --- Step 2: Match against module globs and append entries ---
|
|
462
479
|
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
463
480
|
DEVDOCS_REL=$(yq '.devdocs.path // "devdocs"' "$CONFIG_FILE" 2>/dev/null || echo "devdocs")
|
|
464
|
-
|
|
465
|
-
INDEX_PATH="${
|
|
481
|
+
# Read-only: the index is read from the main checkout; nothing is ever written under ${REPO_PATH}.
|
|
482
|
+
INDEX_PATH="${REPO_PATH}/${DEVDOCS_REL}/index.yaml"
|
|
466
483
|
|
|
467
484
|
if [ ! -f "$INDEX_PATH" ]; then
|
|
468
485
|
echo "Phase C1.7: ${INDEX_PATH} not found — skipping dossier append"
|
|
469
|
-
|
|
486
|
+
return 0
|
|
470
487
|
fi
|
|
471
488
|
|
|
472
489
|
# Extract modules entries: "name|glob|path"
|
|
@@ -474,15 +491,39 @@ MODULE_ENTRIES=$(yq '.modules[]? | .name + "|" + .glob + "|" + .path' "$INDEX_PA
|
|
|
474
491
|
|
|
475
492
|
if [ -z "$MODULE_ENTRIES" ]; then
|
|
476
493
|
echo "Phase C1.7: No modules[] section in index.yaml — skipping dossier append"
|
|
477
|
-
|
|
494
|
+
return 0
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# All dossier writes happen in a temporary detached worktree off origin/<base>, never in ${REPO_PATH}.
|
|
498
|
+
DOSSIER_BASE="${PR_BASE:-staging}"
|
|
499
|
+
DOSSIER_TMP=""
|
|
500
|
+
if ! git -C "${REPO_PATH}" fetch origin "${DOSSIER_BASE}" >/dev/null 2>&1; then
|
|
501
|
+
echo "Phase C1.7: skipped - cannot fetch origin/${DOSSIER_BASE} (no origin remote or no network)"
|
|
502
|
+
return 0
|
|
478
503
|
fi
|
|
504
|
+
DOSSIER_TMP=$(mktemp -d "${TMPDIR:-/tmp}/forge-dossier-{NUMBER}.XXXXXX" 2>/dev/null)
|
|
505
|
+
if [ -z "$DOSSIER_TMP" ] || ! git -C "${REPO_PATH}" worktree add --detach "$DOSSIER_TMP" "origin/${DOSSIER_BASE}" >/dev/null 2>&1; then
|
|
506
|
+
echo "Phase C1.7: skipped - git worktree add failed for origin/${DOSSIER_BASE}"
|
|
507
|
+
[ -n "$DOSSIER_TMP" ] && rm -rf "$DOSSIER_TMP"
|
|
508
|
+
DOSSIER_TMP=""
|
|
509
|
+
return 0
|
|
510
|
+
fi
|
|
511
|
+
# Cleanup guarantee: registered right after a successful worktree add, so an abort,
|
|
512
|
+
# an early return or an interrupt cannot leak the temporary worktree.
|
|
513
|
+
trap '_dossier_cleanup' EXIT
|
|
514
|
+
DEVDOCS_PATH="${DOSSIER_TMP}/${DEVDOCS_REL}"
|
|
479
515
|
|
|
480
516
|
DOSSIER_TIMESTAMP=$(date -u +"%Y-%m-%d")
|
|
481
517
|
DOSSIER_UPDATED_MODULES=""
|
|
482
518
|
|
|
483
519
|
# Iterate module entries; for each: check if any changed file matches the glob
|
|
484
520
|
while IFS='|' read -r MOD_NAME MOD_GLOB MOD_PATH; do
|
|
521
|
+
[ -n "$DOSSIER_TMP" ] || break
|
|
485
522
|
[ -z "$MOD_GLOB" ] || [ -z "$MOD_PATH" ] && continue
|
|
523
|
+
# Reject traversal / absolute module paths: writes must stay inside the temp worktree.
|
|
524
|
+
case "$MOD_PATH" in
|
|
525
|
+
/*|..|../*|*/..|*/../*) echo "Phase C1.7: skipped module '${MOD_NAME}' - unsafe path '${MOD_PATH}'"; continue ;;
|
|
526
|
+
esac
|
|
486
527
|
DOSSIER_ABS="${DEVDOCS_PATH}/${MOD_PATH}"
|
|
487
528
|
|
|
488
529
|
MATCHED=0
|
|
@@ -561,45 +602,68 @@ DOSSIER_INIT_EOF
|
|
|
561
602
|
DOSSIER_UPDATED_MODULES="${DOSSIER_UPDATED_MODULES} ${MOD_NAME}"
|
|
562
603
|
|
|
563
604
|
done <<< "$MODULE_ENTRIES"
|
|
564
|
-
```
|
|
565
605
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if [
|
|
570
|
-
|
|
571
|
-
|
|
606
|
+
# --- Step 3: Commit in the temporary worktree, open a PR, post annotation ---
|
|
607
|
+
DOSSIER_BRANCH="docs/dossier-{NUMBER}"
|
|
608
|
+
DOSSIER_PR_URL=""
|
|
609
|
+
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
610
|
+
echo "DRY_RUN: would commit, push and open dossier PR ${DOSSIER_BRANCH}; skipped (dry-run)"
|
|
611
|
+
elif [ -n "$DOSSIER_TMP" ] && [ -n "$DOSSIER_UPDATED_MODULES" ]; then
|
|
612
|
+
# Stage and commit ONLY inside the temporary worktree ($DOSSIER_TMP), never in ${REPO_PATH}.
|
|
572
613
|
CHANGED_DOSSIER_FILES=$(echo "$DOSSIER_UPDATED_MODULES" | tr ' ' '\n' | while IFS= read -r mod; do
|
|
614
|
+
[ -n "$mod" ] || continue
|
|
573
615
|
yq ".modules[]? | select(.name == \"${mod}\") | \"${DEVDOCS_REL}/\" + .path" "$INDEX_PATH" 2>/dev/null
|
|
574
616
|
done | grep -v '^$')
|
|
575
617
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
618
|
+
while IFS= read -r dossier_file; do
|
|
619
|
+
[ -n "$dossier_file" ] || continue
|
|
620
|
+
git -C "$DOSSIER_TMP" add "$dossier_file" 2>/dev/null || true
|
|
621
|
+
done <<< "$CHANGED_DOSSIER_FILES"
|
|
622
|
+
|
|
623
|
+
if git -C "$DOSSIER_TMP" diff --cached --quiet 2>/dev/null; then
|
|
624
|
+
echo "Phase C1.7: skipped - no staged dossier changes"
|
|
625
|
+
elif ! git -C "$DOSSIER_TMP" checkout -q -B "$DOSSIER_BRANCH" 2>/dev/null; then
|
|
626
|
+
echo "Phase C1.7: skipped - cannot create branch ${DOSSIER_BRANCH}"
|
|
627
|
+
elif ! git -C "$DOSSIER_TMP" commit -s -q -m "docs(dossier): append entry for PR #{PR_NUMBER} (#${NUMBER})" 2>/dev/null; then
|
|
628
|
+
echo "Phase C1.7: skipped - commit failed in temporary worktree"
|
|
629
|
+
elif { git -C "$DOSSIER_TMP" fetch -q origin "$DOSSIER_BRANCH" >/dev/null 2>&1 || true; \
|
|
630
|
+
! git -C "$DOSSIER_TMP" push -u --force-with-lease="refs/heads/${DOSSIER_BRANCH}" origin "$DOSSIER_BRANCH" >/dev/null 2>&1; }; then
|
|
631
|
+
# Retry-safe: the branch is a pipeline-owned throwaway, so a stale remote copy from an earlier attempt is
|
|
632
|
+
# overwritten under a lease (fetched above; absent branch is fine). Never a bare --force.
|
|
633
|
+
echo "Phase C1.7: skipped - push of ${DOSSIER_BRANCH} failed (no push rights, branch protection, or lease rejected)"
|
|
634
|
+
else
|
|
635
|
+
# Reuse an already-open PR for this branch instead of creating a duplicate.
|
|
636
|
+
DOSSIER_PR_URL=$(gh pr list {GH_FLAG} --head "$DOSSIER_BRANCH" --state open --json url --jq '.[0].url // empty' 2>/dev/null || echo "")
|
|
637
|
+
if [ -n "$DOSSIER_PR_URL" ]; then
|
|
638
|
+
echo "Phase C1.7: reusing open dossier PR ${DOSSIER_PR_URL}"
|
|
587
639
|
else
|
|
588
|
-
|
|
640
|
+
DOSSIER_PR_URL=$(gh pr create {GH_FLAG} --base "${DOSSIER_BASE}" --head "$DOSSIER_BRANCH" \
|
|
641
|
+
--title "docs(dossier): append entry for PR #{PR_NUMBER} (#${NUMBER})" \
|
|
642
|
+
--body "Module dossier entry for PR #{PR_NUMBER} (#${NUMBER}):${DOSSIER_UPDATED_MODULES}. Opened by close.md Phase C1.7." 2>/dev/null || echo "")
|
|
643
|
+
[ -n "$DOSSIER_PR_URL" ] || echo "Phase C1.7: skipped - gh pr create failed for ${DOSSIER_BRANCH}"
|
|
589
644
|
fi
|
|
590
645
|
fi
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
# Always remove the temporary worktree (non-fatal); the main checkout was never modified.
|
|
649
|
+
_dossier_cleanup
|
|
591
650
|
|
|
592
|
-
|
|
651
|
+
if [ -n "$DOSSIER_PR_URL" ]; then
|
|
593
652
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DOSSIER_UPDATED -->
|
|
594
653
|
Module dossier(s) updated:${DOSSIER_UPDATED_MODULES}
|
|
595
654
|
|
|
596
|
-
|
|
655
|
+
Dossier PR opened after PR #{PR_NUMBER} merged: ${DOSSIER_PR_URL}. Once merged, future agents working
|
|
597
656
|
on these modules will receive the updated knowledge through the devdocs channel (context.md Phase C-1).
|
|
598
657
|
|
|
599
658
|
<!-- FORGE:DOSSIER_UPDATED:COMPLETE -->" 2>/dev/null || true
|
|
600
659
|
else
|
|
601
|
-
echo "Phase C1.7:
|
|
660
|
+
echo "Phase C1.7: skipped - no dossier PR opened (see reasons above); continuing"
|
|
602
661
|
fi
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
_dossier_run
|
|
665
|
+
_dossier_cleanup
|
|
666
|
+
trap - EXIT
|
|
603
667
|
```
|
|
604
668
|
|
|
605
669
|
---
|
|
@@ -65,14 +65,20 @@ fi
|
|
|
65
65
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
66
66
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
67
67
|
FORGE_ROOT=""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
69
|
+
_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
|
|
70
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
71
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
72
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
73
|
+
# 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.
|
|
74
|
+
_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")")"
|
|
75
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
76
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
77
|
+
# 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)
|
|
78
|
+
_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)"
|
|
79
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
74
80
|
# '${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.
|
|
75
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
81
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
76
82
|
while IFS= read -r _c; do
|
|
77
83
|
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
|
|
78
84
|
done <<< "$_k"
|
|
@@ -80,14 +80,20 @@ fi
|
|
|
80
80
|
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
81
81
|
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
82
82
|
FORGE_ROOT=""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
84
|
+
_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
|
|
85
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
86
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
87
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
88
|
+
# 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.
|
|
89
|
+
_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")")"
|
|
90
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
91
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
92
|
+
# 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)
|
|
93
|
+
_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)"
|
|
94
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
89
95
|
# '${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.
|
|
90
|
-
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
96
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
91
97
|
while IFS= read -r _c; do
|
|
92
98
|
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
|
|
93
99
|
done <<< "$_k"
|
|
@@ -158,6 +158,8 @@ If the worktree/branch checkout fails for any reason (branch deleted, force-push
|
|
|
158
158
|
|
|
159
159
|
For each FIXABLE item from Phase M1: read the affected file(s) in `{WORKTREE_PATH}` before editing (never assume current state), apply the fix. Follow the same implementation discipline as `work-on/build/implement.md` I3 (cross-lane import guard, library-callback verification, deliverable-type consistency, no unrequested scope) — this file does not restate those rules, it inherits them.
|
|
160
160
|
|
|
161
|
+
**Never delete, skip, or weaken a failing check to go green.** Removing, skipping (`skip`, `xfail`, `continue-on-error`, `if: false`, commenting out) or weakening a failing test or CI step is never a valid fix. Fix the cause in the code under test, or classify the item UNFIXABLE, add/re-affirm `needs-human`, and post a comment that names the failing tests and failing assertions (test title plus assertion text from the CI log). The only exception is a deletion justified by removal of the tested code itself (see quality-gate 2U). Enforcement is the quality gate's coverage-reduction check (2U): a `COVERAGE-1` finding is fixed by restoring the test or step, never by suppressing the finding. <!-- Added: forge#3257 -->
|
|
162
|
+
|
|
161
163
|
**If the block reason was a mergeability conflict** (`CONFLICTING`/`DIRTY`/`BLOCKED`): resolve it by rebasing `{HEAD_BRANCH}` onto `origin/{PR_BASE}` (or merging `{PR_BASE}` in, whichever preserves a clean, reviewable history) — resolve conflicts manually, do not blindly take "ours"/"theirs".
|
|
162
164
|
|
|
163
165
|
**Quality Gate** (same loop as Phase 3G, max 3 iterations):
|
|
@@ -232,6 +234,10 @@ Note the marker is `<!-- FORGE:REMEDIATION -->` with **no** `:COMPLETE` suffix y
|
|
|
232
234
|
|
|
233
235
|
## Phase M6: Re-Invoke /review-pr
|
|
234
236
|
|
|
237
|
+
**Dispatch-tool probe (forge#3240 — run FIRST)**: `review-pr` must launch its domain review agents through a sub-agent dispatch tool and refuses to review inline. A forked remediation can be nested deep enough that neither `Task` nor `Agent` is available. Resolve the tool with the identical order `commands/review-pr.md` § "Sub-Agent Dispatch Tool Resolution" uses (`Task`, then `Agent`; OpenCode uses `task`). Do not copy or weaken that section, and never review inline here.
|
|
238
|
+
|
|
239
|
+
If no dispatch tool resolves, a missing tool is not a human decision, so do NOT invoke `review-pr`, do NOT add `needs-human`, and leave `workflow:in-review` in place. Set `RE_GATE_OUTCOME="REREVIEW-REQUIRED"`, skip Phase M7, go to Phase M8 (which posts `FORGE:REMEDIATION:COMPLETE` with a `REREVIEW-REQUIRED` re-gate line), and return `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`. The caller re-runs `/review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch. If a dispatch tool resolves, continue below.
|
|
240
|
+
|
|
235
241
|
```
|
|
236
242
|
if DRY_RUN=true:
|
|
237
243
|
record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
|
|
@@ -255,7 +261,7 @@ else:
|
|
|
255
261
|
|
|
256
262
|
Wait for the completed child result and retain its `REVIEW_RESULT` in remediation state before continuing to Phase M7. A running/progress response is not a completion result. If the child errors or does not return a parseable `REVIEW_RESULT`, stop with `REMEDIATE_RESULT: status: BLOCKED`; do not report remediation in progress as a terminal parent result. The `Skill(...)` invocation above remains the non-OpenCode path.
|
|
257
263
|
|
|
258
|
-
If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate refused to merge on missing phase markers, not on a code finding. Re-run each missing phase named on the `MISSING:` lines via its `Skill(...)`, then re-invoke this phase once. If it is `PHASE_TRAIL_FAILED` again, keep `needs-human`, post `<!-- FORGE:PHASE_TRAIL_FAILED -->` listing the still-missing markers, and exit `REMEDIATE_RESULT: status: BLOCKED`. Never treat it as `HELD-AWAITING-MERGE` or `AUTO-LANDED`.
|
|
264
|
+
If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate refused to merge on missing phase markers, not on a code finding. Re-run each missing phase named on the `MISSING:` lines via its `Skill(...)`, then re-invoke this phase once. If it is `PHASE_TRAIL_FAILED` again, keep `needs-human`, post `<!-- FORGE:PHASE_TRAIL_FAILED -->` listing the still-missing markers, and exit `REMEDIATE_RESULT: status: BLOCKED`. Never treat it as `HELD-AWAITING-MERGE` or `AUTO-LANDED`. A still-failing trail can also be cleared by a human `<!-- FORGE:PHASE_TRAIL_OVERRIDE -->` comment (forge#3152, see `scripts/verify-phase-trail.sh -h`); the verifier decides, this phase never posts one.
|
|
259
265
|
|
|
260
266
|
If the retained `REVIEW_RESULT` is `BLOCKED` with a blocker mentioning the phase trail (e.g. "phase trail unreadable (rc=N)": the Phase 8 verifier exited ≥2 / 127, forge#3147): the gate could not run, so nothing is missing and nothing is re-run. Keep `needs-human` (review-pr Phase 8 already re-asserted it), do not merge, and exit `REMEDIATE_RESULT: status: BLOCKED` with the same blocker.
|
|
261
267
|
|
|
@@ -286,6 +292,8 @@ Re-read the issue's current labels after M6:
|
|
|
286
292
|
POST_REVIEW_LABELS=$(gh issue view {ISSUE_NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(",")')
|
|
287
293
|
```
|
|
288
294
|
|
|
295
|
+
**If `RE_GATE_OUTCOME="REREVIEW-REQUIRED"`** (forge#3240, set in M6): the bar does not apply — no review ran. Skip to Phase M8.
|
|
296
|
+
|
|
289
297
|
**If `needs-human` is present** (re-escalated case): the bar does not apply — nothing to compute. `RE_GATE_OUTCOME="RE-ESCALATED"`. Skip to Phase M8.
|
|
290
298
|
|
|
291
299
|
**If `workflow:awaiting-merge` is present** (clean re-review case — the only branch where `review-pr.md`'s guard has already safely parked this PR): compute the bar.
|
|
@@ -416,6 +424,7 @@ case "$RE_GATE_OUTCOME" in
|
|
|
416
424
|
HELD-AWAITING-MERGE) AUTO_LAND_BAR_TEXT="NOT MET (${APPROVED_COUNT:-0} APPROVED: reviews)"; OUTCOME_DETAIL="at workflow:awaiting-merge" ;;
|
|
417
425
|
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
418
426
|
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|
|
427
|
+
REREVIEW-REQUIRED) AUTO_LAND_BAR_TEXT="N/A — re-review not run (no sub-agent dispatch tool in this session)"; OUTCOME_DETAIL="at workflow:in-review, re-review required from a session with dispatch" ;;
|
|
419
428
|
*) AUTO_LAND_BAR_TEXT="N/A"; OUTCOME_DETAIL="" ;;
|
|
420
429
|
esac
|
|
421
430
|
|
|
@@ -441,6 +450,8 @@ Skill("{FORGE_SKILL_PREFIX}work-on:close", args="{ISSUE_NUMBER} --repo {GH_REPO}
|
|
|
441
450
|
|
|
442
451
|
`work-on:close` handles project board update, final issue body, parent tracker, trajectory log, and worktree cleanup (including the remediation worktree at `{WORKTREE_PATH}`) — do not duplicate any of that here.
|
|
443
452
|
|
|
453
|
+
**If the outcome was `REREVIEW-REQUIRED`** (forge#3240): do not invoke close and do not add `needs-human`. Leave the worktree and `workflow:in-review` in place and return `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`; the caller runs the re-review and, on `AUTO-LANDED`, drives close itself.
|
|
454
|
+
|
|
444
455
|
**If the outcome was `HELD-AWAITING-MERGE`, `RE-ESCALATED`, or `UNFIXABLE`**: leave the worktree in place (a human may need it for manual inspection/merge) and return the structured result below without invoking close. Do not close the issue.
|
|
445
456
|
|
|
446
457
|
---
|
|
@@ -451,12 +462,12 @@ Return this structured block to the caller:
|
|
|
451
462
|
|
|
452
463
|
```
|
|
453
464
|
REMEDIATE_RESULT:
|
|
454
|
-
status: COMPLETE | ALREADY_DONE | UNFIXABLE | BLOCKED
|
|
465
|
+
status: COMPLETE | ALREADY_DONE | UNFIXABLE | BLOCKED | REREVIEW_REQUIRED
|
|
455
466
|
pr_number: {PR_NUMBER}
|
|
456
467
|
issue_number: {ISSUE_NUMBER}
|
|
457
|
-
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | N/A
|
|
468
|
+
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | REREVIEW-REQUIRED | N/A
|
|
458
469
|
findings_addressed: [{finding_number}, ...]
|
|
459
470
|
blocker: {description if status=BLOCKED}
|
|
460
471
|
```
|
|
461
472
|
|
|
462
|
-
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself. For every other `re_gate_outcome`, this result is terminal for the current invocation: the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`). Whether invoked standalone (`/work-on <pr> --remediate`) or via the orchestrator's item 6.4 dispatch, no further action is required from the caller.
|
|
473
|
+
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself. For `re_gate_outcome: REREVIEW-REQUIRED` (`status: REREVIEW_REQUIRED`, forge#3240) the fix is pushed but no review ran because this session has no sub-agent dispatch tool: the caller MUST run `review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch (never inline) and then drive close itself on a merge. For every other `re_gate_outcome`, this result is terminal for the current invocation: the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`). Whether invoked standalone (`/work-on <pr> --remediate`) or via the orchestrator's item 6.4 dispatch, no further action is required from the caller.
|