forgedock 1.0.20 → 1.0.21

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.
@@ -453,6 +453,108 @@ Initial dispatch: #2633, #2636, #2645, #2646 (all ready — launched simultaneou
453
453
 
454
454
  **Key advantage over waves**: When #2633 completes, #2634 dispatches immediately — it does not wait for #2636, #2645, or #2646 to finish. Similarly, when #2645 completes, #2647 dispatches immediately regardless of other issues' status.
455
455
 
456
+ ### Step 3D.5: Cycle Detection (MANDATORY) <!-- Added: forge#1085 -->
457
+
458
+ **Run immediately after Step 3D's DAG edge construction, before presenting the plan (Step 3E).** This step validates that the predecessor graph is acyclic. Without it, mutual `Depends on` declarations (e.g., A depends on B AND B depends on A) cause both issues to remain permanently blocked in Step 4B's dispatch loop — no error, no timeout, indefinite deadlock.
459
+
460
+ **Algorithm**: Kahn's topological sort. Runs in O(V+E) — negligible overhead for typical batch sizes.
461
+
462
+ ```bash
463
+ # --- Step 3D.5: Cycle Detection ---
464
+ # Inputs:
465
+ # ISSUES[] — array of all issue numbers in the DAG
466
+ # PREDECESSORS[N] — space-separated list of predecessor issue numbers for issue N
467
+
468
+ # Step 1: Compute in-degree for each issue
469
+ declare -A IN_DEGREE
470
+ for NUM in "${ISSUES[@]}"; do
471
+ IN_DEGREE[$NUM]=0
472
+ done
473
+ for NUM in "${ISSUES[@]}"; do
474
+ for PRED in ${PREDECESSORS[$NUM]:-}; do
475
+ IN_DEGREE[$NUM]=$(( ${IN_DEGREE[$NUM]:-0} + 1 ))
476
+ done
477
+ done
478
+
479
+ # Step 2: Seed the queue with zero-in-degree issues (no predecessors)
480
+ KAHN_QUEUE=()
481
+ for NUM in "${ISSUES[@]}"; do
482
+ [ "${IN_DEGREE[$NUM]}" -eq 0 ] && KAHN_QUEUE+=("$NUM")
483
+ done
484
+
485
+ # Step 3: Process queue — reduce successor in-degrees, enqueue newly freed issues
486
+ PROCESSED_COUNT=0
487
+ PROCESSED_ORDER=()
488
+ while [ "${#KAHN_QUEUE[@]}" -gt 0 ]; do
489
+ # Dequeue
490
+ CURRENT="${KAHN_QUEUE[0]}"
491
+ KAHN_QUEUE=("${KAHN_QUEUE[@]:1}")
492
+ PROCESSED_ORDER+=("$CURRENT")
493
+ PROCESSED_COUNT=$(( PROCESSED_COUNT + 1 ))
494
+
495
+ # Reduce in-degree of all issues that depend on CURRENT (i.e., CURRENT is in their PREDECESSORS)
496
+ for SUCCESSOR in "${ISSUES[@]}"; do
497
+ for PRED in ${PREDECESSORS[$SUCCESSOR]:-}; do
498
+ if [ "$PRED" = "$CURRENT" ]; then
499
+ IN_DEGREE[$SUCCESSOR]=$(( ${IN_DEGREE[$SUCCESSOR]} - 1 ))
500
+ [ "${IN_DEGREE[$SUCCESSOR]}" -eq 0 ] && KAHN_QUEUE+=("$SUCCESSOR")
501
+ fi
502
+ done
503
+ done
504
+ done
505
+
506
+ # Step 4: Any issue not processed has in-degree > 0 — part of a cycle
507
+ CYCLE_ISSUES=()
508
+ EXCLUDED_CYCLE=()
509
+ for NUM in "${ISSUES[@]}"; do
510
+ FOUND=false
511
+ for P in "${PROCESSED_ORDER[@]}"; do [ "$P" = "$NUM" ] && FOUND=true && break; done
512
+ [ "$FOUND" = "false" ] && CYCLE_ISSUES+=("$NUM")
513
+ done
514
+
515
+ # Step 5: Handle cycles
516
+ if [ "${#CYCLE_ISSUES[@]}" -gt 0 ]; then
517
+ echo "CYCLE DETECTED in dependency graph — the following issues form a circular dependency:"
518
+ for C in "${CYCLE_ISSUES[@]}"; do
519
+ echo " #${C}: predecessors=[${PREDECESSORS[$C]}]"
520
+ # Label each cyclic issue needs-human
521
+ gh issue edit "$C" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
522
+ gh issue comment "$C" -R {GH_REPO} --body "**Cycle detected by /orchestrate**: This issue is part of a circular dependency chain involving issues: ${CYCLE_ISSUES[*]/#/#}. The orchestrator cannot dispatch it automatically. Please fix the \`Depends on\` declarations so that no cycle exists, then re-run /orchestrate." 2>/dev/null || true
523
+ # Remove from DAG — store in EXCLUDED_CYCLE for Step 3E reporting
524
+ EXCLUDED_CYCLE+=("$C")
525
+ # Remove from ISSUES array for all downstream processing
526
+ # Use exact-match filter loop — pattern substitution (${array[@]/pattern}) leaves blank
527
+ # slots and corrupts partial matches (e.g., removing 100 changes 1000 to 0).
528
+ NEW_ISSUES=()
529
+ for I in "${ISSUES[@]}"; do
530
+ [ "$I" != "$C" ] && NEW_ISSUES+=("$I")
531
+ done
532
+ ISSUES=("${NEW_ISSUES[@]}")
533
+ done
534
+ echo ""
535
+ echo "These issues have been labeled needs-human and excluded from the DAG."
536
+ echo "Fix their dependency declarations and re-run /orchestrate."
537
+ else
538
+ echo "DAG cycle check: PASS — no cycles detected. Proceeding with ${#PROCESSED_ORDER[@]} issues."
539
+ fi
540
+
541
+ # Guard: if all issues were cyclic, ISSUES[] is now empty — abort before presenting an empty plan
542
+ if [ "${#ISSUES[@]}" -eq 0 ]; then
543
+ echo ""
544
+ echo "ERROR: All issues in this batch form circular dependencies and have been excluded."
545
+ echo "Every issue has been labeled needs-human."
546
+ echo "Fix the Depends on / Blocked by declarations so no cycle exists, then re-run /orchestrate."
547
+ exit 1
548
+ fi
549
+ # --- End Step 3D.5 ---
550
+ ```
551
+
552
+ **After this step**:
553
+ - `ISSUES[]` contains only acyclic issues — safe to dispatch
554
+ - `EXCLUDED_CYCLE[]` contains cyclic issue numbers — reported in Step 3E, never dispatched
555
+ - If `EXCLUDED_CYCLE` is non-empty, report it clearly in the Step 3E plan before asking for user confirmation
556
+ - If `ISSUES[]` is empty after cycle exclusion (all issues were cyclic), the guard above aborts with `exit 1` — Step 3E is never reached with an empty plan <!-- Added: forge#1110 -->
557
+
456
558
  ### Step 3E: Present the plan to the user
457
559
 
458
560
  ```
@@ -504,6 +606,18 @@ Initial dispatch: #2633, #2636, #2645, #2646 (all ready — launched simultaneou
504
606
  **Excluded** (already in progress / ineligible):
505
607
  - #{X} — {reason}
506
608
 
609
+ {IF EXCLUDED_CYCLE is non-empty:}
610
+ ### ⚠ Circular Dependencies Detected — Manual Fix Required
611
+
612
+ The following issues form a circular dependency chain and **cannot be dispatched** until the cycle is resolved:
613
+
614
+ | Issue | Depends On | Problem |
615
+ |-------|------------|---------|
616
+ {rows: each EXCLUDED_CYCLE issue, its predecessor list, "mutual dependency — forms cycle with #{other}"}
617
+
618
+ **Action required**: Edit each issue's body to remove or correct the `Depends on` / `Blocked by` declarations so no cycle exists. Each issue has been labeled `needs-human`. After fixing, re-run `/orchestrate` to dispatch them.
619
+ {END IF}
620
+
507
621
  Proceed? (yes / adjust / pick specific issues)
508
622
  ```
509
623
 
@@ -648,6 +762,14 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
648
762
  declare -A ISSUE_LANE
649
763
  declare -A ISSUE_PR_BASE
650
764
 
765
+ # Batch-level accumulators for review-finding cascade control (Step 4C) and
766
+ # Completion Sweep (Step 4F). Declared here so they persist across ALL agent
767
+ # completions — Step 4C runs per-agent and must NOT re-initialize these.
768
+ DEFERRED_FINDINGS=()
769
+ QUEUED_FINDINGS=()
770
+ declare -A DEFERRED_REASONS
771
+ declare -A AGENT_ISSUE_MAP
772
+
651
773
  for NUM in {ready_issue_numbers}; do
652
774
  PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
653
775
  echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
@@ -758,6 +880,15 @@ fi
758
880
 
759
881
  If `GIST_CONTEXT` is empty (no parent investigation or milestone index found), the variable resolves to a blank line in the template — no impact on the agent prompt. <!-- Updated: forge#341 -->
760
882
 
883
+ **Capture agent IDs after the batch spawn (MANDATORY)**: Each `Agent(...)` call returns an agent ID. Store each returned ID in `AGENT_ISSUE_MAP` keyed by issue number. This map is the only way to resume a stalled agent by ID in Steps 4B and 4B.5:
884
+
885
+ ```
886
+ # After the single-message batch spawn, capture each returned ID:
887
+ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
888
+ ```
889
+
890
+ `AGENT_ISSUE_MAP` starts empty and accumulates entries as agents are spawned. For parallel dispatch (all Agent() calls in one message), capture the returned IDs from the batch response — one entry per issue — before entering Step 4B's monitoring loop. Without this capture, `resume=` calls in Steps 4B and 4B.5 will have no agent ID to reference and the resume will fail. <!-- Added: forge#1083 -->
891
+
761
892
  **Launch all ready agents simultaneously** by putting multiple Agent tool calls in a single message. Use `run_in_background=true` so they execute in parallel.
762
893
 
763
894
  ### Step 4B: Monitor completions and dispatch newly ready issues
@@ -798,7 +929,7 @@ done
798
929
  2. **If the issue is NOT in a terminal state** (`workflow:merged`, `workflow:invalid`, or `needs-human`), the agent stalled mid-pipeline. **Resume it immediately**:
799
930
  ```
800
931
  Agent(
801
- resume="{AGENT_ID}",
932
+ resume=AGENT_ISSUE_MAP[{NUMBER}],
802
933
  description="Resume #{NUMBER} pipeline",
803
934
  run_in_background=true,
804
935
  prompt="The previous /work-on invocation stopped before completing the full pipeline. The issue is currently at {CURRENT_WORKFLOW_STATE}. Continue — invoke Skill(skill='work-on', args='{NUMBER}') to resume the routing loop from the current state. /work-on will re-read GitHub state and pick up where it left off."
@@ -806,7 +937,7 @@ done
806
937
  ```
807
938
  **Resume ALL stalled agents in a single message** (parallel resume). Do not wait between resumes.
808
939
 
809
- 3. **Track resume cycles per agent.** If an agent has been resumed 3+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
940
+ 3. **Track resume cycles per agent.** If an agent has been resumed 2+ times and still hasn't reached a terminal state, report it as a failure — do not resume again.
810
941
 
811
942
  4. **Record completed results**: Success (PR merged), Invalid (issue closed), Blocked (needs human), or Error
812
943
 
@@ -835,7 +966,7 @@ done
835
966
 
836
967
  9. **Run staging integrity check** (from Step 4A-pre) if the completed agent merged a PR targeting staging.
837
968
 
838
- **Termination condition**: All issues in the DAG are in a terminal state (merged, invalid, needs-human, or skipped due to dependency failure). When this condition is met, proceed to Phase 5.
969
+ **Termination condition**: All issues in the DAG are in a terminal state (merged, invalid, needs-human, or skipped due to dependency failure). When this condition is met, check whether deferred review-spawned findings exist (accumulated in `DEFERRED_FINDINGS` during Step 4C). If deferred findings exist → proceed to Step 4F (Completion Sweep). If no deferred findings → proceed to Phase 5.
839
970
 
840
971
  **Anti-pattern — DO NOT DO THIS:**
841
972
  - `sleep 60/120/180/300` loops to check status — you will be notified automatically
@@ -921,7 +1052,7 @@ Issue #${NUM} has been auto-resumed ${STALL_COUNT} times without reaching a term
921
1052
  done
922
1053
 
923
1054
  # Launch all stall resumes in parallel (single message — same rule as Step 4B)
924
- # For each NUM in $STALL_RESUME_LIST, call Agent(resume="{AGENT_ID}", run_in_background=true, ...)
1055
+ # For each NUM in $STALL_RESUME_LIST, call Agent(resume=AGENT_ISSUE_MAP[NUM], run_in_background=true, ...)
925
1056
  ```
926
1057
 
927
1058
  **Resume all stalled agents in a single message** (parallel). Use the same `Agent(resume=...)` pattern as Step 4B — do not wait between individual resumes.
@@ -989,6 +1120,8 @@ ALL_BATCH_FILES=$(echo "$ALL_BATCH_FILES" | sort -u | grep -v '^$')
989
1120
 
990
1121
  ```bash
991
1122
  # For each finding, check its priority label and generation
1123
+ # NOTE: DEFERRED_FINDINGS, QUEUED_FINDINGS, and DEFERRED_REASONS are declared at
1124
+ # batch scope in Step 4A.pre — do NOT re-initialize them here (Step 4C runs per-agent).
992
1125
  for FINDING_NUM in {spawned_finding_numbers}; do
993
1126
  FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body \
994
1127
  --jq '{labels: [.labels[].name], title: .title, body: .body}')
@@ -1022,6 +1155,7 @@ for FINDING_NUM in {spawned_finding_numbers}; do
1022
1155
 
1023
1156
  if [ "$DEFER" = "true" ]; then
1024
1157
  DEFERRED_FINDINGS+=($FINDING_NUM)
1158
+ DEFERRED_REASONS[$FINDING_NUM]="$DEFER_REASON"
1025
1159
  echo "Deferred #${FINDING_NUM}: $DEFER_REASON"
1026
1160
  else
1027
1161
  QUEUED_FINDINGS+=($FINDING_NUM)
@@ -1043,7 +1177,7 @@ done
1043
1177
 
1044
1178
  **For deferred findings:**
1045
1179
 
1046
- They remain open in GitHub — they will be picked up by future `/orchestrate` or `/work-on` runs. Log them in the final batch summary under "deferred" so they are visible. Do NOT close or label them — leave them for the next pipeline pass.
1180
+ Track them in `DEFERRED_FINDINGS` for re-evaluation in Step 4F (Completion Sweep) after the DAG drains. Do NOT close or label them yet — the sweep will determine their final disposition.
1047
1181
 
1048
1182
  **If no review-finding issues were spawned:** Continue monitoring for the next agent completion.
1049
1183
 
@@ -1191,6 +1325,215 @@ If an agent reports failure or error:
1191
1325
 
1192
1326
  **Do NOT retry failed agents automatically.** Report the failure and let the user decide.
1193
1327
 
1328
+ ### Step 4F: Completion Sweep (deferred review-spawned findings) <!-- Added: forge#1105 -->
1329
+
1330
+ **When to run**: After all DAG issues reach terminal state AND `DEFERRED_FINDINGS` is non-empty. Skip if no findings were deferred during this batch.
1331
+
1332
+ **WHY THIS EXISTS**: Deferred findings accumulate during the batch because of file-overlap and cascade-control heuristics (Step 4C). But once the DAG drains, the conditions that caused deferral often no longer apply — completed issues no longer occupy files, so same-file overlap vanishes. Without this sweep, deferred findings silently pile up across runs and never get resolved.
1333
+
1334
+ **Step 4F.1: Classify deferred findings into permanent vs re-evaluable**
1335
+
1336
+ ```bash
1337
+ PERMANENT_DEFERRED=()
1338
+ SWEEP_CANDIDATES=()
1339
+
1340
+ for FINDING_NUM in "${DEFERRED_FINDINGS[@]}"; do
1341
+ DEFER_REASON="${DEFERRED_REASONS[$FINDING_NUM]}"
1342
+
1343
+ # Generation >= 2 deferrals are PERMANENT — unbounded cascade prevention
1344
+ if echo "$DEFER_REASON" | grep -qi "generation"; then
1345
+ PERMANENT_DEFERRED+=($FINDING_NUM)
1346
+ else
1347
+ # All other deferrals (comment/typo, P3 same-file) are re-evaluable
1348
+ SWEEP_CANDIDATES+=($FINDING_NUM)
1349
+ fi
1350
+ done
1351
+
1352
+ echo "Completion sweep: ${#SWEEP_CANDIDATES[@]} re-evaluable, ${#PERMANENT_DEFERRED[@]} permanent"
1353
+ ```
1354
+
1355
+ **Step 4F.2: Re-evaluate sweep candidates**
1356
+
1357
+ Re-run the Step 4C heuristics against the now-empty DAG. Since all original batch issues are in terminal state, the `ALL_BATCH_FILES` list for file-overlap detection is empty — P3 same-file deferrals will now pass.
1358
+
1359
+ ```bash
1360
+ SWEEP_EXECUTE=()
1361
+ SWEEP_STILL_DEFERRED=()
1362
+
1363
+ for FINDING_NUM in "${SWEEP_CANDIDATES[@]}"; do
1364
+ FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body,state \
1365
+ --jq '{labels: [.labels[].name], title: .title, body: .body, state: .state}')
1366
+
1367
+ # Skip if already closed (resolved by another process)
1368
+ STATE=$(echo "$FINDING_DATA" | jq -r '.state')
1369
+ [ "$STATE" = "CLOSED" ] && continue
1370
+
1371
+ PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
1372
+ TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
1373
+
1374
+ # Re-apply heuristics against the drained DAG (no active batch files)
1375
+ # Comment/typo heuristic still applies — these are cosmetic regardless of DAG state
1376
+ if echo "$TITLE" | grep -qi "comment\|typo"; then
1377
+ SWEEP_STILL_DEFERRED+=($FINDING_NUM)
1378
+ echo "Sweep: #${FINDING_NUM} still deferred (comment/typo — cosmetic)"
1379
+ else
1380
+ # P3 same-file overlap no longer applies (DAG is drained, no active files)
1381
+ # All other findings are safe to execute
1382
+ SWEEP_EXECUTE+=($FINDING_NUM)
1383
+ echo "Sweep: #${FINDING_NUM} cleared for execution (file overlap resolved)"
1384
+ fi
1385
+ done
1386
+ ```
1387
+
1388
+ **Step 4F.3: Dispatch cleared findings**
1389
+
1390
+ For each finding in `SWEEP_EXECUTE`, add it to a fresh sweep DAG and dispatch using the same Steps 4A.pre.0, 4A.pre, and 4A logic. Run file-overlap detection between the swept findings themselves (they may conflict with each other).
1391
+
1392
+ **MANDATORY**: Use the full Step 4A agent template verbatim for each swept finding. Do NOT use a bare `prompt="Run /work-on N"` — that bypasses the label-state loop contract, source branch detection, and all pipeline enforcement rules. Swept findings are always `review-finding` issues that require source branch detection to route correctly.
1393
+
1394
+ **Step 4F.3.pre: Classify lane for each sweep finding (MANDATORY before dispatching)**
1395
+
1396
+ Run `classify-lane.sh` per finding — same pattern as Step 4A.pre:
1397
+
1398
+ ```bash
1399
+ declare -A SWEEP_LANE
1400
+ declare -A SWEEP_PR_BASE
1401
+
1402
+ for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
1403
+ SWEEP_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$FINDING_NUM" -R {GH_REPO}) || {
1404
+ echo "ERROR: classify-lane.sh failed for #$FINDING_NUM — adding needs-human and skipping" >&2
1405
+ gh issue edit "$FINDING_NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
1406
+ continue
1407
+ }
1408
+ if [ "$SWEEP_BASE" = "staging" ]; then
1409
+ SWEEP_LANE[$FINDING_NUM]="fast-lane"
1410
+ else
1411
+ SWEEP_LANE[$FINDING_NUM]="feature-lane"
1412
+ fi
1413
+ SWEEP_PR_BASE[$FINDING_NUM]="$SWEEP_BASE"
1414
+ echo "#$FINDING_NUM → lane=${SWEEP_LANE[$FINDING_NUM]}, PR_BASE=$SWEEP_BASE"
1415
+ done
1416
+ ```
1417
+
1418
+ **Step 4F.3.dispatch: Dispatch with full Step 4A template**
1419
+
1420
+ ```bash
1421
+ if [ ${#SWEEP_EXECUTE[@]} -gt 0 ]; then
1422
+ echo "Completion sweep: dispatching ${#SWEEP_EXECUTE[@]} cleared findings"
1423
+
1424
+ # Build sweep DAG — same conflict detection as Step 3C Layers 1-4
1425
+ # but only among the swept findings (no original batch issues remain active)
1426
+ # Dispatch ready findings, monitor completions using the same Step 4B loop
1427
+ # This is a SINGLE pass — findings spawned during the sweep are NOT swept again
1428
+ # (they follow the standard Step 4C triage: queued or deferred for next run)
1429
+
1430
+ # NOTE: Step 4A.pre.0 (milestone branch pre-creation) is intentionally skipped here.
1431
+ # Swept findings are always review-finding issues that target an already-existing branch
1432
+ # (staging or a milestone branch). The branch was created when the original batch ran —
1433
+ # it always exists by the time the sweep runs.
1434
+
1435
+ for FINDING_NUM in "${SWEEP_EXECUTE[@]}"; do
1436
+ # Skip any finding whose classify-lane call failed (needs-human already set above)
1437
+ [ -z "${SWEEP_PR_BASE[$FINDING_NUM]:-}" ] && continue
1438
+
1439
+ FINDING_TITLE=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
1440
+
1441
+ # Build GIST_CONTEXT for sweep finding — same as Step 4A generation block
1442
+ GIST_CONTEXT=""
1443
+ PARENT_INV=$(gh issue view "$FINDING_NUM" -R {GH_REPO} --json body --jq '.body' \
1444
+ | grep -oP '(?i)parent[: ]*#\K\d+|spawned from[: ]*#\K\d+' | head -1)
1445
+
1446
+ if [ -n "$PARENT_INV" ] && [ -n "${INVESTIGATION_GISTS[$PARENT_INV]:-}" ]; then
1447
+ GIST_CONTEXT="
1448
+ **CONTEXT FROM PRIOR INVESTIGATION**: Investigation #${PARENT_INV} produced Knowledge Gist(s) with findings relevant to this issue:
1449
+ $(echo "${INVESTIGATION_GISTS[$PARENT_INV]}" | while IFS= read -r url; do echo "- ${url}"; done)
1450
+ Fetch the Gist content during the context-gathering phase for implementation guidance."
1451
+ fi
1452
+
1453
+ if [ -n "$MILESTONE_INDEX_URL" ]; then
1454
+ GIST_CONTEXT="${GIST_CONTEXT}
1455
+
1456
+ **MILESTONE KNOWLEDGE INDEX**: All investigation findings for this milestone are aggregated in a single index Gist:
1457
+ - ${MILESTONE_INDEX_URL}
1458
+ The context-gathering phase can fetch this index to discover all investigation Gists for the milestone."
1459
+ fi
1460
+
1461
+ # Use the full Step 4A template verbatim — copied here so sweep agents receive
1462
+ # the complete pipeline contract. Keep in sync with Step 4A when the template changes.
1463
+ Agent(
1464
+ subagent_type="general-purpose",
1465
+ model="sonnet",
1466
+ description="Work on {PROJECT_PREFIX}#${FINDING_NUM}",
1467
+ run_in_background=true,
1468
+ prompt="You are working on GitHub issue #${FINDING_NUM} for the {PROJECT_NAME} project.
1469
+
1470
+ **Project**: {PROJECT_NAME}
1471
+ **Repository**: {GH_REPO}
1472
+ **Repo path**: {REPO_PATH}
1473
+
1474
+ **YOUR MISSION**: Invoke \`/work-on\` via the Skill tool and let it run to completion. \`/work-on\` is a self-contained routing loop that handles the ENTIRE pipeline: investigate → build (context → architect → implement → validate) → review (push → PR → /review-pr --auto-merge) → close (project board → trajectory log → worktree cleanup). Do NOT intervene, compensate, or manually close issues — \`/work-on\` handles everything including issue closure and label updates in its close phase.
1475
+
1476
+ **CRITICAL — DO NOT STOP EARLY**: /work-on runs as a multi-phase routing loop. Each phase (investigate, build, review, close) returns an intermediate result — these are NOT completion signals. You are NOT done until the issue reaches a terminal state: \`workflow:merged\`, \`workflow:invalid\`, or \`needs-human\`. If /work-on returns after only one phase (e.g., investigation), you MUST invoke it again immediately — it will re-read GitHub state and continue to the next phase. Keep invoking /work-on until it reaches a terminal state. Never output 'done' or stop after an intermediate result.
1477
+
1478
+ **HOW REVIEW FINDINGS WORK**: /review-pr may create GitHub issues (with \`review-finding\` label) for findings it discovers. These are NOT blockers — they are separate work items that will go through their own /work-on pipeline later. The original PR should ALWAYS merge after review. The only exception is build errors (code doesn't compile) — those must be fixed before merging.
1479
+
1480
+ **IMPORTANT RULES**:
1481
+ - **MANDATORY**: You MUST use the Skill tool to invoke 'work-on' with args '${FINDING_NUM}'. Do NOT implement manually — /work-on handles the full pipeline including label state machine (workflow:investigating → workflow:building → workflow:in-review → workflow:merged), investigation reports, PR creation, and cleanup.
1482
+ - For default repo issues: \`Skill(skill='work-on', args='${FINDING_NUM}')\`
1483
+ - For satellite repo issues: \`Skill(skill='work-on', args='{SATELLITE_PREFIX}:${FINDING_NUM}')\` (prefix from forge.yaml → repos.satellites)
1484
+ - NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
1485
+ - NEVER target \`main\` for PRs targeting the default repo. Use \`{STAGING_BRANCH}\` for fast-lane issues, or \`milestone/{slug}\` for milestone issues.
1486
+ - Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to \`main\` for those.
1487
+ - If the issue is INVALID after investigation, close it with a comment explaining why
1488
+ - If you hit merge conflicts or blockers, post a comment on the issue and STOP — do not force anything
1489
+ - Do not interact with the user — you are running autonomously in the background
1490
+ - **NEVER ask the user questions** — you are a background agent. If review finds issues, auto-fix simple ones and proceed. For complex findings, merge anyway and create follow-up issues.
1491
+
1492
+ **LABEL-STATE LOOP CONTRACT — enforce after EVERY Skill return**:
1493
+ After EVERY \`Skill(skill='work-on', ...)\` call returns, immediately check the issue's current workflow label:
1494
+ \`\`\`bash
1495
+ gh issue view ${FINDING_NUM} -R {GH_REPO} --json labels --jq '[.labels[].name | select(startswith(\"workflow:\"))]'
1496
+ \`\`\`
1497
+ **Terminal labels** (only these allow you to stop): \`workflow:merged\`, \`workflow:invalid\`
1498
+ **Terminal condition also**: \`needs-human\` label present OR issue state is \`closed\`
1499
+ If the label is NOT terminal (e.g., \`workflow:investigating\`, \`workflow:ready-to-build\`, \`workflow:building\`, \`workflow:in-review\`), invoke \`Skill(skill='work-on', args='${FINDING_NUM}')\` again immediately. The \`/work-on\` skill will re-read GitHub state and advance to the next phase. Do NOT output a summary, do NOT pause, do NOT ask for confirmation — just invoke it again.
1500
+
1501
+ **CRITICAL — SOURCE BRANCH DETECTION**:
1502
+ - If the issue has the \`review-finding\` label, read the issue body for \`**Code branch**: \\\`{branch}\\\`\`
1503
+ - If found, that is the SOURCE_BRANCH — the code ONLY exists on that branch (e.g., \`staging\`), NOT on \`origin/main\`
1504
+ - Investigation MUST use \`git show origin/{SOURCE_BRANCH}:{filepath}\` to verify the code exists
1505
+ - Worktree MUST branch from \`origin/{SOURCE_BRANCH}\`, NOT \`origin/main\`
1506
+ - PR target is \`{SOURCE_BRANCH}\` (the fix goes back to where the code lives)
1507
+
1508
+ **LANE**: ${SWEEP_LANE[$FINDING_NUM]} (PR target: ${SWEEP_PR_BASE[$FINDING_NUM]})
1509
+ **Issue title**: ${FINDING_TITLE}
1510
+ ${GIST_CONTEXT}
1511
+ "
1512
+ )
1513
+ done
1514
+
1515
+ # Monitor sweep agents using the same Step 4B completion loop
1516
+ # IMPORTANT: Findings spawned by sweep agents are NOT re-swept —
1517
+ # they follow standard Step 4C triage to prevent recursive cascades
1518
+ fi
1519
+ ```
1520
+
1521
+ **Step 4F.4: Report sweep results**
1522
+
1523
+ ```
1524
+ Completion Sweep Results:
1525
+ Dispatched: #{A}, #{B} (file overlap cleared after DAG drain)
1526
+ Still deferred (cosmetic): #{C} (comment/typo)
1527
+ Permanently deferred (gen2): #{D} (generation >= 2 cascade cap)
1528
+ ```
1529
+
1530
+ **After sweep agents complete** (or if no findings were dispatched): proceed to Phase 5.
1531
+
1532
+ **Anti-patterns — DO NOT DO THIS:**
1533
+ - Re-sweeping findings spawned during the sweep itself — this creates unbounded recursion. Sweep is a single pass.
1534
+ - Overriding generation >= 2 deferrals — the cascade cap is absolute.
1535
+ - Skipping the sweep because "there are only a few" deferred findings — even one deferred finding represents unresolved work.
1536
+
1194
1537
  ---
1195
1538
 
1196
1539
  ## Phase 5: Post-Batch Cleanup
@@ -1278,14 +1621,28 @@ Aggregate into the batch-level analytics for Step 6B.
1278
1621
  {IF review findings were created during any agent run:}
1279
1622
  | # | Finding | Source Issue | Source PR | Status |
1280
1623
  |---|---------|-------------|-----------|--------|
1281
- | #{F1} | {title} | #{A} | PR #{PR} | ✓ Merged (in-batch) / ⏳ Open (deferred) |
1624
+ | #{F1} | {title} | #{A} | PR #{PR} | ✓ Merged (in-batch) / ✓ Swept (completion sweep) / ⏳ Deferred (cosmetic) / ⛔ Deferred (gen2 cascade cap) |
1282
1625
 
1283
1626
  {ELSE: "No review findings created during this batch."}
1284
1627
 
1628
+ ### Completion Sweep <!-- Added: forge#1105 -->
1629
+ - **Re-evaluated**: {N} deferred findings re-checked after DAG drain
1630
+ - **Dispatched**: {N} findings cleared and executed (file overlap resolved)
1631
+ - **Still deferred (cosmetic)**: {N} comment/typo findings (low-value, safe to leave)
1632
+ - **Permanently deferred (gen2)**: {N} generation >= 2 findings (cascade cap — requires manual `/work-on`)
1633
+
1634
+ {IF permanently deferred > 0:}
1635
+ **Action required**: The following findings were permanently deferred due to the generation >= 2 cascade cap. They will NOT be picked up automatically — run `/work-on #{N}` manually or include them in the next `/orchestrate` batch:
1636
+ {list of permanently deferred issue numbers and titles}
1637
+
1638
+ {IF cosmetic deferred > 0:}
1639
+ **Low-priority leftovers**: The following cosmetic findings remain open. They will be picked up by future runs or can be batched with `/orchestrate #{N1} #{N2}`:
1640
+ {list of cosmetic deferred issue numbers and titles}
1641
+
1285
1642
  ### Summary
1286
1643
  - **Investigations**: {N} completed, spawned {M} new issues
1287
- - **Review findings**: {N} spawned, {M} resolved in-batch, {K} deferred
1288
- - **Succeeded**: {N} issues resolved (implementation)
1644
+ - **Review findings**: {N} spawned, {M} resolved in-batch, {K} swept, {J} deferred (cosmetic), {L} deferred (gen2)
1645
+ - **Succeeded**: {N} issues resolved (implementation + sweep)
1289
1646
  - **Failed**: {N} issues need attention
1290
1647
  - **Skipped**: {N} issues (dependency failures)
1291
1648
 
@@ -48,6 +48,7 @@ This quality gate uses **domain detection** to adapt to your project's actual te
48
48
  | INFRA | Dockerfiles or entrypoints with runtime UID changes |
49
49
  | ROUTER_BUG | Router Python files where gate conditions are narrowed |
50
50
  | FORGE_GRAPH | `commands/*.md` specs or `scripts/*` files change (ForgeDock self-consistency) |
51
+ | CONFIG_SCHEMA | Config files for external tools (`traefik/`, `infra/nginx/`, `k8s/`, `terraform/`, `*.conf`, `*.toml` in infra paths, or `docker-compose*.yml` with service definition changes) |
51
52
 
52
53
  **Stack-agnostic coverage**: The example domains above reflect common patterns caught in production; they are not requirements. A Go or Ruby project benefits from SECURITY checks. A Node.js project benefits from FRONTEND, PROXY, and DEPLOY checks. Any project benefits from DATABASE and WORKFLOW checks when those file types are present. The stack-specific examples (Python routers, SOPS secrets chain, FastAPI layouts, appleboy SSH deploys) are illustrative — the domain detection system applies the applicable subset to any codebase.
53
54
 
@@ -100,6 +101,7 @@ Before running checks, classify the changed files into domains. This avoids runn
100
101
  | `Dockerfile*` or `entrypoint*.sh` with added/changed `USER`, `su-exec`, `gosu`, or `setuid` | INFRA |
101
102
  | `*.py` in `routers/` where the diff removes a line containing an or-gate condition (lines starting with `-` containing `if.*or`) | ROUTER_BUG |
102
103
  | `commands/*.md` or `scripts/*` files (ForgeDock repo only — dogfoods its own spec graph) | FORGE_GRAPH |
104
+ | Files under `traefik/`, `infra/nginx/`, `k8s/`, `terraform/`, or files matching `*.conf`, `*.toml` in infra paths, or `docker-compose*.yml` with service definition changes | CONFIG_SCHEMA |
103
105
 
104
106
  **Apply the classification:**
105
107
 
@@ -168,6 +170,19 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'route
168
170
  git diff HEAD -- "$f" 2>/dev/null | grep -E '^-.*\bif\b.*\bor\b' | grep -v '^---' | grep -q . && DOMAINS="$DOMAINS ROUTER_BUG" && break
169
171
  done
170
172
 
173
+ # Check for external tool config files (structural schema validation advisory)
174
+ for f in {CHANGED_FILES}; do
175
+ case "$f" in
176
+ traefik/*|infra/nginx/*|k8s/*|terraform/*)
177
+ DOMAINS="$DOMAINS CONFIG_SCHEMA" && break ;;
178
+ *.conf|*.toml)
179
+ echo "$f" | grep -qE 'infra/|traefik/|nginx/|k8s/|terraform/' && DOMAINS="$DOMAINS CONFIG_SCHEMA" && break ;;
180
+ docker-compose*.yml)
181
+ # Trigger only if service definitions changed (not just env vars or labels)
182
+ git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+\s+(image:|command:|entrypoint:)' | grep -q . && DOMAINS="$DOMAINS CONFIG_SCHEMA" && break ;;
183
+ esac
184
+ done
185
+
171
186
  # SECURITY is always included for any code file
172
187
  DOMAINS="SECURITY $DOMAINS"
173
188
 
@@ -190,7 +205,7 @@ Run ONLY the checks whose domain was identified in Step 1.5. Skip checks for dom
190
205
  - **2D (Database)**: Run if `DATABASE` in DOMAINS
191
206
  - **2E (Frontend lifecycle)**: Run if `FRONTEND` in DOMAINS
192
207
  - **2F (Frontend proxy)**: Run if `PROXY` in DOMAINS
193
- - **2G (Cross-service integration)**: Run if `SHELL` in DOMAINS
208
+ - **2G (Cross-service integration)**: Run if `SHELL` or `FORGE_GRAPH` in DOMAINS
194
209
  - **2H (Asyncio cancellation)**: Run if `CONCURRENCY` in DOMAINS
195
210
  - **2I (State completeness)**: Run if `STATE` in DOMAINS
196
211
  - **2J (String literal consistency)**: Run if `STRING_SETS` in DOMAINS
@@ -199,6 +214,7 @@ Run ONLY the checks whose domain was identified in Step 1.5. Skip checks for dom
199
214
  - **2M (Import resolution)**: Run if `IMPORT_RESOLUTION` in DOMAINS
200
215
  - **2N (Runtime UID × filesystem write)**: Run if `INFRA` in DOMAINS
201
216
  - **2O (Residual pattern check)**: Run if `ROUTER_BUG` in DOMAINS
217
+ - **2P (External tool config schema)**: Run if `CONFIG_SCHEMA` in DOMAINS
202
218
 
203
219
  ### 2A: Security (ALL files)
204
220
 
@@ -836,6 +852,68 @@ done
836
852
  - Only triggers when a condition line was actually removed in the diff (not just modified)
837
853
  - Reports file:line matches from sibling files only (excludes the fixed file itself)
838
854
 
855
+ ### 2P: External Tool Config Schema Advisory (external tool config files)
856
+
857
+ **Triggered when**: CONFIG_SCHEMA domain is set (config files for external tools are in the diff).
858
+
859
+ **Why this matters**: External tools with strict config schemas (Traefik, nginx, Kubernetes, Terraform, Docker Compose) silently ignore or reject structurally incorrect config — no startup crash, no error log, the feature simply does not activate. The quality gate cannot run `traefik validate` / `nginx -t` / `terraform validate` (those belong in CI), but it can verify that CI validation steps exist for the tool and remind the builder that structural validation is CI's responsibility. <!-- Added: forge#1104 -->
860
+
861
+ ```bash
862
+ # Identify which external tools have config files in the diff
863
+ TOOL_FILES=$(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E \
864
+ "(traefik/|infra/nginx/|k8s/|terraform/|docker-compose.*\.yml$|.*\.(conf|toml)$)" | head -20)
865
+
866
+ if [ -n "$TOOL_FILES" ]; then
867
+ echo "CONFIG_SCHEMA: External tool config files detected in diff:"
868
+ echo "$TOOL_FILES"
869
+ echo ""
870
+
871
+ # Check whether CI has a validation step for each detected tool
872
+ WORKFLOW_DIR="{WORKTREE_PATH}/.github/workflows"
873
+ CI_MISSING=""
874
+
875
+ for f in $TOOL_FILES; do
876
+ case "$f" in
877
+ traefik/*)
878
+ grep -rlE "traefik.*validate|traefik.*check" "$WORKFLOW_DIR" 2>/dev/null | grep -q . || \
879
+ CI_MISSING="$CI_MISSING traefik"
880
+ ;;
881
+ infra/nginx/*|*nginx*.conf)
882
+ grep -rlE "nginx.*-t|nginx.*test|nginx.*configtest" "$WORKFLOW_DIR" 2>/dev/null | grep -q . || \
883
+ CI_MISSING="$CI_MISSING nginx"
884
+ ;;
885
+ k8s/*|*.yaml)
886
+ if ! grep -rlE "kubectl.*--dry-run|kubeval|kustomize.*build|helm.*lint" "$WORKFLOW_DIR" 2>/dev/null | grep -q .; then
887
+ echo "$f" | grep -qE '^k8s/' && CI_MISSING="$CI_MISSING kubernetes"
888
+ fi
889
+ ;;
890
+ terraform/*)
891
+ grep -rlE "terraform.*validate|terraform.*plan|tflint" "$WORKFLOW_DIR" 2>/dev/null | grep -q . || \
892
+ CI_MISSING="$CI_MISSING terraform"
893
+ ;;
894
+ esac
895
+ done
896
+
897
+ # Deduplicate
898
+ CI_MISSING=$(echo "$CI_MISSING" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ')
899
+
900
+ if [ -n "$CI_MISSING" ]; then
901
+ for tool in $CI_MISSING; do
902
+ echo "CFG-1 | MEDIUM | .github/workflows/ | No CI validation step found for $tool config changes — add structural validation (e.g., 'traefik validate', 'nginx -t', 'terraform validate') to CI so structural errors are caught before merge. The INFRA review agent verifies logical correctness; CI must verify structural correctness."
903
+ done
904
+ else
905
+ echo "CFG: CI validation steps detected for modified tool configs — structural validation covered by CI."
906
+ fi
907
+ fi
908
+ ```
909
+
910
+ **Flag as findings**:
911
+ - No CI validation step for a tool whose config files changed: **MEDIUM** — structural config errors will not be caught before deploy. The fix is a CI-layer addition (see `/ci-audit` for a comprehensive CI gap audit), not a quality gate fix.
912
+
913
+ **This check is advisory (MEDIUM), not blocking (HIGH)**. The quality gate cannot substitute for CI-level validators (`traefik validate`, `nginx -t`, `terraform validate`). Its role is to detect when those validators are absent from CI and surface that gap to the builder. Actual structural validation belongs in GitHub Actions.
914
+
915
+ **Separation of concerns**: ForgeDock reasons about architecture, logic, and security; CI runs deterministic tool validators with zero token cost. The quality gate enforces that the CI layer is complete — it does not replace it.
916
+
839
917
  ## Step 3: Compile findings
840
918
 
841
919
  Format findings as a structured list:
@@ -142,6 +142,7 @@ Append this block at the very end of your comment (after the `---` footer line,
142
142
  | API Design | `API` |
143
143
  | Database & Migration | `DB` |
144
144
  | Infrastructure | `INFRA` |
145
+ | Config Schema | `CFG` |
145
146
 
146
147
  ### Example
147
148
 
@@ -1605,6 +1606,52 @@ If no infra context is configured above, derive the deployment model from the ch
1605
1606
 
1606
1607
  Host-side port bindings to Docker containers are unreliable in SSH deploy contexts. Deploy scripts that invoke database tools directly cannot depend on `localhost:5432` being reachable; the same operation via `docker exec` bypasses the host network entirely and always succeeds when the container is running. <!-- Added: forge#322 -->
1607
1608
 
1609
+ 15. **External tool config schema validation** (when any config file consumed by an external tool is in the diff — trigger: files under `traefik/`, `infra/nginx/`, `k8s/`, `terraform/`, `*.conf`, `*.toml`, or any `docker-compose*.yml` with non-trivial changes to service definitions):
1610
+
1611
+ **Principle: Logical correctness ≠ structural correctness.** A config can have the right values in the wrong nesting and be silently rejected by the external tool — producing zero error logs in some tools (e.g. Traefik v3 ignores unrecognized nesting silently). The INFRA agent must verify both dimensions:
1612
+ - **Logical correctness** (covered by existing checks): Are the values coherent? Do names match across files? Is the wiring consistent with the deploy pipeline?
1613
+ - **Structural correctness** (this check): Does the YAML/TOML/HCL structure match what the tool's actual schema expects? Are directives nested at the correct depth? Do required parent keys exist?
1614
+
1615
+ **For each external tool config file in the diff**:
1616
+
1617
+ 1. **Identify the tool and version**: Read the `image:` tag in `docker-compose*.yml` for the tool (e.g., `traefik:v3.1`, `nginx:1.25`). If the tool version cannot be determined from the diff, note it as unknown.
1618
+
1619
+ 2. **Reason about the schema**: Ask — "Does this change's structure match what `{tool} {version}` actually expects?" Key questions:
1620
+ - Is this directive/key nested under the correct parent section?
1621
+ - Does this version of the tool use a different config syntax than a prior version? (Version migrations often change required nesting — e.g., Traefik v2 → v3 moved many config keys to different parent sections)
1622
+ - Are any required sibling keys missing from the changed section?
1623
+ - Does the tool expect this value at file scope, or under a named block?
1624
+
1625
+ 3. **Cross-reference against sibling config files**: If the project uses dynamic config loading (e.g., `traefik/dynamic/`), verify that a key defined in the static config is not also required in the dynamic config (or vice versa) for the feature to activate.
1626
+
1627
+ 4. **Flag structural mismatches**: If any key or nesting does not match the tool's expected schema for the declared version, flag as **CONFIRMED HIGH**. Structural config mismatches are silently rejected at runtime — no startup crash, no error log, the feature simply does not activate.
1628
+
1629
+ ```bash
1630
+ # Identify tool config files in the diff
1631
+ CONFIG_TOOL_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E \
1632
+ "(traefik/|infra/nginx/|k8s/|terraform/|.*\.conf$|.*\.toml$)" | head -20)
1633
+ COMPOSE_INFRA_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "docker-compose.*\.yml$")
1634
+
1635
+ # For each config file: read the full file (not just the diff) to understand the structure
1636
+ for f in $CONFIG_TOOL_FILES; do
1637
+ echo "=== Reading full config structure: $f ==="
1638
+ cat "$f"
1639
+ done
1640
+
1641
+ # Identify tool version from docker-compose (for version-aware schema reasoning)
1642
+ for f in $COMPOSE_INFRA_FILES; do
1643
+ grep -E "image:\s*(traefik|nginx|haproxy|envoy|caddy|terraform):" "$f" 2>/dev/null && echo " ^ in $f"
1644
+ done
1645
+ ```
1646
+
1647
+ **Severity classification**:
1648
+ - Config key present but under wrong parent section: **CONFIRMED HIGH** — key is silently ignored; feature does not activate
1649
+ - Required sibling key missing from changed section: **CONFIRMED HIGH** — partial config is silently incomplete
1650
+ - Version mismatch (config uses v2 syntax on v3 tool): **CONFIRMED HIGH** — v3 silently ignores v2 keys in most tools
1651
+ - Structural ambiguity (key could plausibly be correct but version cannot be determined): **LIKELY MEDIUM** — flag for human verification with schema reference
1652
+
1653
+ **Do NOT rely solely on logical correctness checks when structural correctness has not been verified.** A config that names services correctly, uses coherent values, and matches the deploy script's expectations can still be silently rejected if the nesting does not match the tool's schema. Both dimensions must be verified independently. <!-- Added: forge#1104 -->
1654
+
1608
1655
  ## Post Findings
1609
1656
  ```bash
1610
1657
  gh pr comment [PR_NUMBER] --body "$(cat <<'EOF'
@@ -1679,5 +1726,6 @@ EOF
1679
1726
  | appleboy/ssh-action Go template safety | Item 12 | COVERED | #226 |
1680
1727
  | Insecure config file defaults | Item 13 | COVERED | #301 |
1681
1728
  | Host-side database tool invocations | Item 14 | COVERED | #322 |
1729
+ | External tool config schema validation (structural correctness) | Item 15 | COVERED | #1104 |
1682
1730
  | Docker image tag pinning (mutable tags) | — | GAP | |
1683
1731
  | Network policy / inter-container isolation | — | GAP | |