forgedock 1.0.2 → 1.0.4

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.
@@ -19,13 +19,14 @@ Read `forge.yaml` at the project root to resolve all project-specific variables
19
19
 
20
20
  ```bash
21
21
  # Parse forge.yaml for project context
22
- GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
22
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
23
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
23
24
  GH_FLAG="-R $GH_REPO"
24
- REPO_PATH=$(yq '.paths.root' forge.yaml)
25
- STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
26
- PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
27
- PROJECT_NUMBER=$(yq '.project_board.project_number // "1"' forge.yaml)
28
- PROJECT_ID=$(yq '.project_board.project_id' forge.yaml)
25
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
26
+ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
27
+ PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
28
+ PROJECT_NUMBER=$(yq '.project_board.project_number // "1"' "$CONFIG_FILE")
29
+ PROJECT_ID=$(yq '.project_board.project_id' "$CONFIG_FILE")
29
30
  ```
30
31
 
31
32
  All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD_OWNER}`, `{PROJECT_NUMBER}`, and `{PROJECT_ID}` references below are populated from `forge.yaml`.
@@ -14,7 +14,8 @@ Tracks the size of every Forge command file over time. Detects bloat by comparin
14
14
  Read project home from `forge.yaml` before running any mode:
15
15
 
16
16
  ```bash
17
- FORGE_HOME=$(yq e '.paths.root' forge.yaml)
17
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
18
+ FORGE_HOME=$(yq e '.paths.root' "$CONFIG_FILE")
18
19
  echo "Forge home: $FORGE_HOME"
19
20
  ```
20
21
 
@@ -55,7 +55,7 @@ fi
55
55
  ### 1A: Locate and read
56
56
 
57
57
  ```bash
58
- FORGE_YAML="$(pwd)/forge.yaml"
58
+ FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
59
59
  if [ ! -f "$FORGE_YAML" ]; then
60
60
  echo "forge.yaml not found in current directory."
61
61
  echo ""
package/commands/issue.md CHANGED
@@ -38,10 +38,11 @@ The user's input (`$ARGUMENTS`) can be:
38
38
  Read `forge.yaml` to build the routing table dynamically:
39
39
 
40
40
  ```bash
41
- GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
41
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
42
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
42
43
  GH_FLAG="-R $GH_REPO"
43
- REPO_PATH=$(yq '.paths.root' forge.yaml)
44
- STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
44
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
45
+ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
45
46
  # Satellite repos from forge.yaml → repos.satellites (each has: prefix, repo, staging_branch)
46
47
  ```
47
48
 
@@ -424,8 +425,8 @@ fi
424
425
 
425
426
  ```bash
426
427
  # Add to project board if forge.yaml → project_board is configured
427
- PROJECT_NUMBER=$(yq '.project_board.project_number // ""' forge.yaml)
428
- PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
428
+ PROJECT_NUMBER=$(yq '.project_board.project_number // ""' "$CONFIG_FILE")
429
+ PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
429
430
  if [ -n "$PROJECT_NUMBER" ] && [ "$PROJECT_NUMBER" != "null" ]; then
430
431
  gh project item-add "$PROJECT_NUMBER" --owner "$PROJECT_BOARD_OWNER" --url "${ISSUE_URL}" 2>/dev/null || true
431
432
  fi
@@ -19,14 +19,15 @@ Read `forge.yaml` at the project root to resolve all project-specific variables
19
19
 
20
20
  ```bash
21
21
  # Parse forge.yaml for project context
22
- GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
22
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
23
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
23
24
  GH_FLAG="-R $GH_REPO"
24
- REPO_PATH=$(yq '.paths.root' forge.yaml)
25
- PROJECT_NAME=$(yq '.project.name' forge.yaml)
26
- STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
27
- PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
28
- PROJECT_NUMBER=$(yq '.project_board.project_number // "1"' forge.yaml)
29
- PROJECT_ID=$(yq '.project_board.project_id' forge.yaml)
25
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
26
+ PROJECT_NAME=$(yq '.project.name' "$CONFIG_FILE")
27
+ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
28
+ PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
29
+ PROJECT_NUMBER=$(yq '.project_board.project_number // "1"' "$CONFIG_FILE")
30
+ PROJECT_ID=$(yq '.project_board.project_id' "$CONFIG_FILE")
30
31
  # Build satellite repo map from repos.satellites list
31
32
  # Each satellite: { prefix, repo, staging_branch }
32
33
  ```
@@ -231,7 +232,7 @@ for ISSUE_NUM in {created_issue_numbers}; do
231
232
  if [ -n "$ITEM_ID" ]; then
232
233
  gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_STATUS_FIELD_ID} --single-select-option-id {PROJECT_BOARD_STATUS_TODO_ID} 2>/dev/null || true # Status=Todo
233
234
  gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_LANE_FIELD_ID} --single-select-option-id {PROJECT_BOARD_LANE_FEATURE_ID} 2>/dev/null || true # Lane=Feature
234
- gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_COMPONENT_FIELD_ID} --single-select-option-id {COMPONENT_OPTION_ID} 2>/dev/null || true # Component (from forge.yaml → project_board.fields)
235
+ gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_COMPONENT_FIELD_ID} --single-select-option-id {COMPONENT_OPTION_ID} 2>/dev/null || true # Component (from forge.yaml → project_board.field_ids)
235
236
  gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_PRIORITY_FIELD_ID} --single-select-option-id {PRIORITY_OPTION_ID} 2>/dev/null || true # Priority (from label)
236
237
  fi
237
238
  done
@@ -568,7 +569,7 @@ Syncs the milestone branch with the latest default branch to pick up fast-lane f
568
569
 
569
570
  ```bash
570
571
  cd {REPO_PATH}
571
- DEFAULT_BRANCH=$(yq '.branches.default' forge.yaml)
572
+ DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
572
573
  git fetch origin $DEFAULT_BRANCH milestone/{slug}
573
574
  git checkout milestone/{slug}
574
575
  git merge origin/$DEFAULT_BRANCH --no-edit
@@ -31,11 +31,12 @@ Read `forge.yaml` at the project root to resolve all project-specific variables
31
31
 
32
32
  ```bash
33
33
  # Parse forge.yaml for project context
34
- GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
34
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
35
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
35
36
  GH_FLAG="-R $GH_REPO"
36
- REPO_PATH=$(yq '.paths.root' forge.yaml)
37
- PROJECT_NAME=$(yq '.project.name' forge.yaml)
38
- STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
37
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
38
+ PROJECT_NAME=$(yq '.project.name' "$CONFIG_FILE")
39
+ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
39
40
  # Build satellite repo map from repos.satellites list
40
41
  # Each satellite: { prefix, repo, staging_branch }
41
42
  ```
@@ -14,8 +14,9 @@ You are the Forge pipeline's self-awareness layer. Your job is to measure how we
14
14
  Read project identity from `forge.yaml` before running any phase:
15
15
 
16
16
  ```bash
17
- FORGE_REPO=$(yq e '.project.owner + "/" + .project.repo' forge.yaml)
18
- FORGE_HOME=$(yq e '.paths.root' forge.yaml)
17
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
18
+ FORGE_REPO=$(yq e '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
19
+ FORGE_HOME=$(yq e '.paths.root' "$CONFIG_FILE")
19
20
  echo "Forge repo: $FORGE_REPO"
20
21
  echo "Forge home: $FORGE_HOME"
21
22
  ```
@@ -783,7 +783,7 @@ If 0 findings:
783
783
  ```
784
784
  QUALITY GATE: PASS — no issues detected.
785
785
 
786
- <!-- FORGE:PHASE_COMPLETE — Quality gate is an intermediate check. See work-on.md Universal Phase Dispatcher for next phase. Not terminal — calling agent must continue. -->
786
+ **YOU MUST NOW continue to sub-phase 3H (Format and verify) in work-on.md — this PASS is intermediate, NOT terminal. Do NOT stop.**
787
787
  ```
788
788
 
789
789
  If findings exist:
@@ -791,5 +791,5 @@ If findings exist:
791
791
  QUALITY GATE FINDINGS: N issues detected.
792
792
  {findings list}
793
793
 
794
- <!-- FORGE:PHASE_COMPLETE — Quality gate is an intermediate check. See work-on.md Universal Phase Dispatcher for next phase. Not terminal — calling agent must continue. -->
794
+ **YOU MUST NOW fix each HIGH and MEDIUM finding, then re-run the quality gate — this result is intermediate, NOT terminal. Do NOT stop.**
795
795
  ```
@@ -108,6 +108,22 @@ REVIEW_SHA=$(gh pr view $ARGUMENTS --json headRefOid --jq '.headRefOid')
108
108
  REVIEW_SHA_SHORT=$(echo "$REVIEW_SHA" | cut -c1-7)
109
109
  gh pr diff $ARGUMENTS --name-only
110
110
  gh pr diff $ARGUMENTS
111
+
112
+ # Mergeability check — GitHub computes this asynchronously; retry up to 3× on UNKNOWN
113
+ # <!-- Added: forge#194 -->
114
+ MERGE_HEALTH_RESULT=$(gh pr view $ARGUMENTS --json mergeable,mergeStateStatus --jq '"\\(.mergeable)|\\(.mergeStateStatus)"')
115
+ MERGE_HEALTH=${MERGE_HEALTH_RESULT%%|*}
116
+ MERGE_HEALTH_STATE=${MERGE_HEALTH_RESULT##*|}
117
+ MERGE_RETRY=0
118
+ while [ "$MERGE_HEALTH" = "UNKNOWN" ] && [ "$MERGE_RETRY" -lt 3 ]; do
119
+ MERGE_RETRY=$((MERGE_RETRY + 1))
120
+ sleep 5
121
+ MERGE_HEALTH_RESULT=$(gh pr view $ARGUMENTS --json mergeable,mergeStateStatus --jq '"\\(.mergeable)|\\(.mergeStateStatus)"')
122
+ MERGE_HEALTH=${MERGE_HEALTH_RESULT%%|*}
123
+ MERGE_HEALTH_STATE=${MERGE_HEALTH_RESULT##*|}
124
+ done
125
+ # MERGE_HEALTH: MERGEABLE | CONFLICTING | UNKNOWN (still async after retries)
126
+ # MERGE_HEALTH_STATE: CLEAN | DIRTY | BLOCKED | UNSTABLE | UNKNOWN
111
127
  ```
112
128
 
113
129
  ### 1B: Classify
@@ -508,7 +524,7 @@ Read the file: $FORGE_HOME/commands/review-pr-agents.md
508
524
  **>>> LOAD CONFIG: Read forge.yaml for project context:**
509
525
  ```bash
510
526
  # Read review config from forge.yaml (if present in project root)
511
- FORGE_YAML="${REPO_ROOT}/forge.yaml"
527
+ FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
512
528
  PROJECT_NAME=$(yq '.project.name' "$FORGE_YAML" 2>/dev/null || echo "this project")
513
529
  PROJECT_CONTEXT=$(yq '.review.context' "$FORGE_YAML" 2>/dev/null || echo "")
514
530
  TECH_STACK=$(yq '.review.tech_stack' "$FORGE_YAML" 2>/dev/null || echo "")
@@ -752,7 +768,7 @@ for FINDING_NUM in {numbers}; do
752
768
  # PROJECT_ID = forge.yaml → project_board.project_id
753
769
  # STATUS_FIELD_ID = forge.yaml → project_board.field_ids.status
754
770
  # LANE_FIELD_ID = forge.yaml → project_board.field_ids.lane
755
- # REVIEW_FINDING_OPTION_ID = forge.yaml → project_board.option_ids.status.in_review
771
+ # REVIEW_FINDING_OPTION_ID = forge.yaml → project_board.option_ids.workflow.in_review
756
772
  ITEM_ID=$(gh project item-add ${PROJECT_NUMBER} --owner ${OWNER} --url "https://github.com/${GH_REPO}/issues/${FINDING_NUM}" --format json --jq '.id' 2>/dev/null)
757
773
  [ -n "$ITEM_ID" ] && {
758
774
  [ -n "$STATUS_FIELD_ID" ] && gh project item-edit --project-id ${PROJECT_ID} --id "$ITEM_ID" --field-id ${STATUS_FIELD_ID} --single-select-option-id ${REVIEW_FINDING_OPTION_ID} 2>/dev/null || true
@@ -836,16 +852,31 @@ Verdict determined by standard blocking criteria.
836
852
  1. Phase 2 automated checks failed (build error, type error, test failure)
837
853
  2. Agent finding is CONFIRMED at HIGH or CRITICAL severity
838
854
  3. **[Milestone PRs only]** Phase 7A Purpose Regression Gate flagged `HAS_PURPOSE_REGRESSION=true` for this finding — regardless of whether it causes a runtime error
855
+ 4. `MERGE_HEALTH == "CONFLICTING"` OR `MERGE_HEALTH_STATE` in {`DIRTY`, `BLOCKED`} — PR cannot be merged cleanly into its base branch <!-- Added: forge#194 -->
856
+ - Verdict: CHANGES REQUESTED. Message: "Merge conflict with `{base}`. Rebase `{head}` onto `origin/{base}`, resolve the conflicting files, then re-run /review-pr."
857
+ - If `MERGE_HEALTH == "UNKNOWN"` after retries: emit a WARNING in the verdict body (do NOT treat as a block — GitHub may still be computing it).
839
858
 
840
859
  ```bash
860
+ # Determine if mergeability is a blocker (MERGE_HEALTH/MERGE_HEALTH_STATE set in Phase 1A; BASE/HEAD set in Phase 0 Mode 3)
861
+ HAS_MERGE_CONFLICT=false
862
+ MERGE_CONFLICT_MSG=""
863
+ if [ "$MERGE_HEALTH" = "CONFLICTING" ] || [ "$MERGE_HEALTH_STATE" = "DIRTY" ] || [ "$MERGE_HEALTH_STATE" = "BLOCKED" ]; then
864
+ HAS_MERGE_CONFLICT=true
865
+ MERGE_CONFLICT_MSG="Merge conflict with \`${BASE}\`. Rebase \`${HEAD}\` onto \`origin/${BASE}\`, resolve the conflicting files, then re-run /review-pr."
866
+ fi
867
+
841
868
  # Stale review:
842
869
  gh pr review $ARGUMENTS --comment --body "Review of commit $REVIEW_SHA_SHORT is stale — PR HEAD changed. Re-run /review-pr."
843
870
 
844
871
  # Clean (no blocking issues):
845
- gh pr review $ARGUMENTS --comment --body "APPROVED: commit $REVIEW_SHA_SHORT after context-aware review ([N] agents: [names]). [M] findings created as issues. Safe to merge."
872
+ gh pr review $ARGUMENTS --comment --body "APPROVED: commit $REVIEW_SHA_SHORT after context-aware review ([N] agents: [names]). [M] findings created as issues. Safe to merge.
873
+ $([ "$MERGE_HEALTH" = "UNKNOWN" ] && echo "
874
+ ⚠ Mergeability: GitHub is still computing merge state (UNKNOWN after retries). Verify manually before merging.")"
846
875
 
847
- # Blocking issues (including purpose regressions):
876
+ # Blocking issues (including merge conflicts and purpose regressions):
848
877
  gh pr review $ARGUMENTS --comment --body "CHANGES REQUESTED: commit $REVIEW_SHA_SHORT — [N] blocking issues found. See GitHub issues.
878
+ $([ "$HAS_MERGE_CONFLICT" = "true" ] && echo "
879
+ 🔴 Merge Conflict: ${MERGE_CONFLICT_MSG}")
849
880
  $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
850
881
  ⚠ Purpose Regression: [N] finding(s) contradict the milestone's stated goal and are automatically blocking regardless of runtime impact. See: ${PURPOSE_REGRESSION_FINDINGS[@]}")"
851
882
  ```
@@ -857,15 +888,28 @@ $([ "$HAS_PURPOSE_REGRESSION" = "true" ] && echo "
857
888
  **Skip if** `AUTO_MERGE=false`.
858
889
 
859
890
  ```bash
860
- # Checkpoint comment on issue
861
- gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: {VERDICT}. Proceeding to merge."
891
+ # Pre-merge mergeability guard — re-fetch fresh state before attempting merge <!-- Added: forge#194 -->
892
+ # A PR that was MERGEABLE at Phase 1A may have become CONFLICTING if the base branch
893
+ # received commits while the review was running. Re-check before every auto-merge.
894
+ PRE_MERGE_RESULT=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json mergeable,mergeStateStatus --jq '"\\(.mergeable)|\\(.mergeStateStatus)"')
895
+ PRE_MERGE_HEALTH=${PRE_MERGE_RESULT%%|*}
896
+ PRE_MERGE_HEALTH_STATE=${PRE_MERGE_RESULT##*|}
862
897
 
863
- # Merge
864
- gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge
898
+ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIRTY" ] || [ "$PRE_MERGE_HEALTH_STATE" = "BLOCKED" ]; then
899
+ gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "⛔ Auto-merge aborted for PR #{PR_NUMBER}: PR is not mergeable (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`). Rebase the branch onto \`{MERGE_BASE}\` and resolve conflicts, then re-run /review-pr."
900
+ gh issue edit {MERGE_ISSUE} {MERGE_GH_FLAG} --add-label "needs-human" 2>/dev/null || true
901
+ # STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
902
+ else
903
+ # Checkpoint comment on issue
904
+ gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: {VERDICT}. Proceeding to merge."
905
+
906
+ # Merge
907
+ gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge
865
908
 
866
- # Verify
867
- MERGE_STATE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json state --jq '.state')
868
- [ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
909
+ # Verify
910
+ MERGE_STATE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json state --jq '.state')
911
+ [ "$MERGE_STATE" != "MERGED" ] && gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "PR #{PR_NUMBER} merge failed. State: $MERGE_STATE."
912
+ fi
869
913
  ```
870
914
 
871
915
  **Important**: Phase 8 ONLY merges the PR. It does NOT close the issue, update labels, or clean up worktrees. When invoked via `/work-on`, those responsibilities belong to `work-on/close.md` — which runs after the router detects state 4 (MERGE_COMPLETE: PR merged + issue open). Doing them here would cause the router to hit TERMINAL_MERGED (state 1) and skip the close phase entirely.
@@ -19,9 +19,10 @@ Before any phase runs, build the ecosystem registry from `forge.yaml → repos.s
19
19
  # Read satellite repos from forge.yaml
20
20
  # Each entry provides: prefix, repo, staging_branch, local_path
21
21
  # (Optional: package, publish_workflow — read from each satellite's package.json / workflow files)
22
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
22
23
 
23
24
  # Gate: if repos section is absent or has no satellites, stop here
24
- if [ -z "$(yq '.repos.satellites // empty' forge.yaml 2>/dev/null)" ]; then
25
+ if [ -z "$(yq '.repos.satellites // empty' "$CONFIG_FILE" 2>/dev/null)" ]; then
25
26
  echo "No satellite repos configured in forge.yaml → repos.satellites."
26
27
  echo "Add entries to forge.yaml to use /sync-ecosystem:"
27
28
  echo ""
@@ -52,10 +53,10 @@ fi
52
53
  # node_sdk: "sdk/node"
53
54
  # Then {REPO_PATH}/sdk/python and {REPO_PATH}/sdk/node are the per-SDK roots.
54
55
 
55
- PROJECT_NAME=$(yq '.project.name' forge.yaml)
56
- PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
57
- PROJECT_BOARD_NUMBER=$(yq '.project_board.project_number // empty' forge.yaml)
58
- PROJECT_BOARD_ID=$(yq '.project_board.project_id // empty' forge.yaml)
56
+ PROJECT_NAME=$(yq '.project.name' "$CONFIG_FILE")
57
+ PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
58
+ PROJECT_BOARD_NUMBER=$(yq '.project_board.project_number // empty' "$CONFIG_FILE")
59
+ PROJECT_BOARD_ID=$(yq '.project_board.project_id // empty' "$CONFIG_FILE")
59
60
  ```
60
61
 
61
62
  ---
@@ -110,13 +110,14 @@ Update the project board to reflect the merged state. This replaces the old Phas
110
110
 
111
111
  ```bash
112
112
  # Read project board config from forge.yaml
113
- PROJECT_BOARD_OWNER=$(yq '.project_board.owner // ""' forge.yaml 2>/dev/null || echo "")
114
- PROJECT_ID=$(yq '.project_board.project_id // ""' forge.yaml 2>/dev/null || echo "")
115
- PROJECT_NUMBER=$(yq '.project_board.project_number // ""' forge.yaml 2>/dev/null || echo "")
116
- STATUS_FIELD_ID=$(yq '.project_board.field_ids.status // ""' forge.yaml 2>/dev/null || echo "")
117
- WORKFLOW_FIELD_ID=$(yq '.project_board.field_ids.workflow // ""' forge.yaml 2>/dev/null || echo "")
118
- STATUS_DONE_OPTION_ID=$(yq '.project_board.option_ids.status.done // ""' forge.yaml 2>/dev/null || echo "")
119
- WORKFLOW_MERGED_OPTION_ID=$(yq '.project_board.option_ids.workflow.merged // ""' forge.yaml 2>/dev/null || echo "")
113
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
114
+ PROJECT_BOARD_OWNER=$(yq '.project_board.owner // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
115
+ PROJECT_ID=$(yq '.project_board.project_id // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
116
+ PROJECT_NUMBER=$(yq '.project_board.project_number // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
117
+ STATUS_FIELD_ID=$(yq '.project_board.field_ids.status // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
118
+ WORKFLOW_FIELD_ID=$(yq '.project_board.field_ids.workflow // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
119
+ STATUS_DONE_OPTION_ID=$(yq '.project_board.option_ids.status.done // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
120
+ WORKFLOW_MERGED_OPTION_ID=$(yq '.project_board.option_ids.workflow.merged // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
120
121
 
121
122
  if [ -z "$PROJECT_BOARD_OWNER" ] || [ -z "$PROJECT_ID" ] || [ -z "$STATUS_FIELD_ID" ]; then
122
123
  echo "INFO: project_board not configured in forge.yaml — skipping board update"
@@ -219,7 +219,9 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
219
219
 
220
220
  ## Output
221
221
 
222
- Output this structured block — the routing loop in `work-on.md` will read this result, re-evaluate state, and continue to the next phase. This subcommand is complete; control returns to the router's loop iteration.
222
+ **After posting this result, immediately proceed to the close subcommand — do NOT stop here. `REVIEW_RESULT: status: COMPLETE` is an intermediate result, NOT a terminal state. The pipeline is not done. You MUST invoke `Skill("work-on:close", ...)` now to close the issue, update labels to `workflow:merged`, post the trajectory log, and clean up the worktree.**
223
+
224
+ Output this structured block:
223
225
 
224
226
  ```
225
227
  REVIEW_RESULT:
@@ -741,9 +741,9 @@ if iteration == max_iterations AND not PASS:
741
741
  Add needs-human label → STOP
742
742
  ```
743
743
 
744
- <!-- FORGE:PHASE_COMPLETE — Quality gate done. See Universal Phase Dispatcher: next sub-phase is 3H. Not terminal — continue immediately. -->
744
+ # MUST CONTINUE to sub-phase 3H (Format and verify) — quality gate PASS is intermediate, NOT terminal. <!-- Added: forge#220 -->
745
745
 
746
- **After quality gate completes (PASS or fixes applied): proceed to the next sub-phase per the Universal Phase Dispatcher. Quality gate is an intermediate check — "PASS" means the code is clean, NOT that the build is done.**
746
+ **After quality gate completes (PASS or fixes applied): proceed immediately to sub-phase 3H below. Quality gate is an intermediate check — "PASS" means the code is clean, NOT that the build is done. Do NOT stop.**
747
747
 
748
748
  ### 3H: Format and verify
749
749
 
@@ -974,6 +974,8 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
974
974
 
975
975
  <!-- FORGE:PHASE_COMPLETE — Review done, PR merged. See Universal Phase Dispatcher: next phase is Phase 6 (Close & Cleanup). Not terminal — continue immediately. -->
976
976
 
977
+ **After /review-pr returns and the PR is confirmed merged: immediately proceed to Phase 6 (Close & Cleanup). Do NOT stop here. `REVIEW_RESULT: status: COMPLETE` is an intermediate result — the pipeline is NOT done. Invoke Phase 6 now to close the issue, update labels, post the trajectory log, and clean up the worktree.**
978
+
977
979
  ---
978
980
 
979
981
  ## Phase 6: Close & Cleanup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"