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.
- package/README.md +16 -1
- package/commands/ci-audit.md +469 -0
- package/commands/orchestrate.md +365 -8
- package/commands/quality-gate.md +79 -1
- package/commands/review-pr-agents.md +48 -0
- package/docs/comparison.md +135 -0
- package/docs/design/developer-objections-ai-context-tools.md +244 -0
- package/docs/design/messaging-angles-research.md +179 -0
- package/docs/design/show-hn-draft.md +86 -0
- package/package.json +2 -2
package/commands/orchestrate.md
CHANGED
|
@@ -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=
|
|
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
|
|
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=
|
|
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
|
-
|
|
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) / ⏳
|
|
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
|
|
package/commands/quality-gate.md
CHANGED
|
@@ -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 | |
|