forgedock 1.0.5 → 1.0.7
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 +7 -1
- package/bin/forgedock.mjs +496 -364
- package/bin/labels.json +152 -0
- package/commands/audit-agents.md +4 -2
- package/commands/autopilot.md +19 -11
- package/commands/cleanup.md +27 -12
- package/commands/deploy-info.md +36 -17
- package/commands/incident-response.md +15 -10
- package/commands/issue.md +8 -6
- package/commands/milestone.md +97 -48
- package/commands/orchestrate.md +109 -65
- package/commands/pipeline-health.md +8 -1
- package/commands/qa-sweep.md +14 -3
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +129 -23
- package/commands/review-pr.md +237 -90
- package/commands/rollback.md +26 -7
- package/commands/security-audit.md +27 -25
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +154 -2
- package/commands/work-on/build/context.md +131 -3
- package/commands/work-on/build/validate.md +32 -13
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on-monolithic.md +9 -5
- package/commands/work-on.md +67 -23
- package/docs/CONFIG.md +117 -0
- package/package.json +2 -1
- package/scripts/verify-route-registration.sh +36 -15
- package/scripts/verify-sops-chain.sh +18 -0
- package/templates/devdocs/README.md +96 -0
- package/templates/devdocs/agent/using-forgedock.md +245 -0
- package/templates/devdocs/agent/using-github.md +325 -0
- package/templates/devdocs/project/architecture.md +122 -0
- package/templates/devdocs/project/conventions.md +158 -0
- package/templates/devdocs/project/custom-instructions.md +73 -0
- package/templates/devdocs/project/glossary.md +84 -0
- package/templates/devdocs/project/stack.md +80 -0
- package/templates/devdocs/templates/commit-convention.md +147 -0
- package/templates/devdocs/templates/issue.md +141 -0
- package/templates/devdocs/templates/pull-request.md +115 -0
- package/templates/devdocs/templates/review-finding.md +147 -0
- package/bin/github-app-manifest.json +0 -24
package/commands/milestone.md
CHANGED
|
@@ -25,14 +25,23 @@ GH_FLAG="-R $GH_REPO"
|
|
|
25
25
|
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
26
26
|
PROJECT_NAME=$(yq '.project.name' "$CONFIG_FILE")
|
|
27
27
|
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
28
|
+
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
28
29
|
PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
|
|
29
|
-
PROJECT_NUMBER=$(yq '.project_board.project_number //
|
|
30
|
-
PROJECT_ID=$(yq '.project_board.project_id' "$CONFIG_FILE")
|
|
30
|
+
PROJECT_NUMBER=$(yq '.project_board.project_number // empty' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
31
|
+
PROJECT_ID=$(yq '.project_board.project_id // empty' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
32
|
+
# Project board field and option IDs — empty string when project_board section is absent
|
|
33
|
+
STATUS_FIELD_ID=$(yq '.project_board.field_ids.status // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
34
|
+
LANE_FIELD_ID=$(yq '.project_board.field_ids.lane // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
35
|
+
COMPONENT_FIELD_ID=$(yq '.project_board.field_ids.component // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
36
|
+
PRIORITY_FIELD_ID=$(yq '.project_board.field_ids.priority // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
37
|
+
STATUS_TODO_OPTION_ID=$(yq '.project_board.option_ids.status.todo // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
38
|
+
LANE_FEATURE_OPTION_ID=$(yq '.project_board.option_ids.lane.feature // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
39
|
+
PRIORITY_OPTION_ID="" # Resolved per-issue from the issue's priority label — see Step 6B
|
|
31
40
|
# Build satellite repo map from repos.satellites list
|
|
32
41
|
# Each satellite: { prefix, repo, staging_branch }
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD_OWNER}`, `{PROJECT_NUMBER}`, and `{
|
|
44
|
+
All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, `{DEFAULT_BRANCH}`, `{PROJECT_BOARD_OWNER}`, `{PROJECT_NUMBER}`, `{PROJECT_ID}`, `{STATUS_FIELD_ID}`, `{LANE_FIELD_ID}`, `{COMPONENT_FIELD_ID}`, `{PRIORITY_FIELD_ID}`, `{STATUS_TODO_OPTION_ID}`, and `{LANE_FEATURE_OPTION_ID}` references below are populated from `forge.yaml`.
|
|
36
45
|
|
|
37
46
|
---
|
|
38
47
|
|
|
@@ -101,40 +110,45 @@ Capture the milestone number from the response.
|
|
|
101
110
|
|
|
102
111
|
```bash
|
|
103
112
|
cd {REPO_PATH}
|
|
104
|
-
git fetch origin
|
|
105
|
-
git branch milestone/{slug} origin
|
|
113
|
+
git fetch origin $DEFAULT_BRANCH
|
|
114
|
+
git branch milestone/{slug} origin/$DEFAULT_BRANCH
|
|
106
115
|
git push origin milestone/{slug}
|
|
107
116
|
```
|
|
108
117
|
|
|
109
118
|
### Step 4: Scope decomposition
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
**Investigation-gated decomposition** — do NOT skip code reads. The single-pass shallow-planning approach (one agent, whole milestone, no per-issue code reads) produces under-specified issues that ship incomplete. This step mandates per-proposed-issue investigation before any issue body is written. See `work-on/decompose.md` Phase D0 for the reference pattern that enforces this gate. <!-- Added: forge#293 -->
|
|
121
|
+
|
|
122
|
+
**4A: First pass — enumerate proposed issues (no code reads yet)**
|
|
123
|
+
|
|
124
|
+
Analyze the milestone description and produce a preliminary list of proposed issues. At this stage, only the title, type, and rough dependency order are needed. Do NOT write issue bodies yet.
|
|
125
|
+
|
|
126
|
+
**4B: Per-issue investigation (MANDATORY before writing any issue body)**
|
|
127
|
+
|
|
128
|
+
For each proposed issue in the preliminary list, MUST read the actual code before writing the issue body:
|
|
129
|
+
|
|
130
|
+
1. **Identify affected files**: Read the files the issue will touch. Start with the files named in the milestone description, then expand to callers and related modules.
|
|
131
|
+
2. **Enumerate all call sites**: For coverage/refactor tasks, grep for every occurrence of the pattern being changed. Do NOT estimate — enumerate. Example: `grep -rn 'poetry run\|npx ' commands/ --include='*.md' | wc -l` to count call sites before writing a "make X config-driven" issue.
|
|
132
|
+
3. **Write exhaustive acceptance criteria**: For coverage/refactor tasks, acceptance criteria MUST be falsifiable. A `grep`/absence assertion qualifies (e.g., `` `grep -rn 'hardcoded_value' commands/` returns no matches ``). The number of criteria must match the scope — there is no cap. Two criteria for a 14-call-site refactor is not acceptable.
|
|
133
|
+
4. **Identify override/companion files**: If a file needs a change, its companion files (config overrides, prod variants, sibling modules) almost certainly do too. List all of them.
|
|
134
|
+
|
|
135
|
+
**4C: Output format — per-issue, after code read**
|
|
136
|
+
|
|
137
|
+
For each proposed issue, after completing the per-issue investigation in Step 4B:
|
|
138
|
+
|
|
139
|
+
- **Title**: Actionable, specific (conventional commit prefix: `fix:`, `feat:`, `refactor:`)
|
|
140
|
+
- **Repo**: Which repo this issue belongs to (default or satellite prefix from forge.yaml)
|
|
141
|
+
- **Type**: `feature`, `bug`, `refactor`, `infra`
|
|
142
|
+
- **Priority**: P1-P3
|
|
143
|
+
- **Size**: S (1-2 files), M (3-5 files), L (6+ files)
|
|
144
|
+
- **Dependencies**: Which other issues must be done first (by number)
|
|
145
|
+
- **Affected files**: Full list of files enumerated during the Step 4B code read (not estimated)
|
|
146
|
+
- **Acceptance criteria**: Exhaustive and falsifiable. For coverage/refactor tasks: one criterion per affected call site or pattern, written as a grep/absence assertion. No cap on criteria count — the list must be complete enough that a reviewer can verify coverage without reading the implementation.
|
|
147
|
+
|
|
148
|
+
**Project ecosystem context** (from `forge.yaml`):
|
|
149
|
+
- **Primary repo** (`{GH_REPO}`): The main project codebase at `{REPO_PATH}`
|
|
150
|
+
- **Tech stack**: {review.tech_stack from forge.yaml, or read CLAUDE.md for project context}
|
|
151
|
+
- **Satellite repos**: Read from `forge.yaml → repos.satellites` (each has a prefix and repo)
|
|
138
152
|
|
|
139
153
|
### Step 5: Review with user
|
|
140
154
|
|
|
@@ -144,10 +158,14 @@ Present the proposed issues to the user. Ask:
|
|
|
144
158
|
|
|
145
159
|
### Step 6: Create issues and assign to milestone
|
|
146
160
|
|
|
161
|
+
**Issue body standard**: Use the **Pipeline Issue Template** from `issue.md` Phase 3D as the body structure for every issue created here. Do NOT use a bespoke inline template — the Pipeline Issue Template is the single canonical standard for all automated issue creation across the pipeline. The body content (Problem, Root Cause, Affected Files, Acceptance Criteria, Context, Dependencies) comes from the per-issue investigation in Step 4B. <!-- Added: forge#293 -->
|
|
162
|
+
|
|
147
163
|
For each approved issue, create it in the **correct repo** based on the scope analysis:
|
|
148
164
|
|
|
149
165
|
```bash
|
|
150
166
|
# For default repo issues:
|
|
167
|
+
# Body content derives from the Step 4B per-issue investigation.
|
|
168
|
+
# Structure MUST match the Pipeline Issue Template in issue.md Phase 3D.
|
|
151
169
|
gh issue create {GH_FLAG} \
|
|
152
170
|
--title "{fix|feat|refactor}: {issue_title}" \
|
|
153
171
|
--label "{type},{priority}" \
|
|
@@ -155,20 +173,21 @@ gh issue create {GH_FLAG} \
|
|
|
155
173
|
--body "$(cat <<'BODY_EOF'
|
|
156
174
|
## Problem
|
|
157
175
|
|
|
158
|
-
{1-3 sentences: what needs to be built or fixed for this milestone.
|
|
176
|
+
{1-3 sentences: what needs to be built or fixed for this milestone. Specific — derived from Step 4B code read, not milestone description alone.}
|
|
159
177
|
|
|
160
178
|
## Root Cause (if known)
|
|
161
179
|
|
|
162
|
-
{Why this needs to change — architecture gap, missing feature, technical debt. If a new feature: "New capability required for {MILESTONE_TITLE}."}
|
|
180
|
+
{Why this needs to change — architecture gap, missing feature, technical debt. Reference specific file:line where possible. If a new feature: "New capability required for {MILESTONE_TITLE}."}
|
|
163
181
|
|
|
164
182
|
## Affected Files
|
|
165
183
|
|
|
166
|
-
Files that need changes:
|
|
184
|
+
Files that need changes (full list from Step 4B code read — ordered by dependency):
|
|
167
185
|
1. `{filepath}` — {what needs to change}
|
|
168
186
|
2. `{filepath}` — {what needs to change}
|
|
169
187
|
|
|
170
188
|
## Acceptance Criteria
|
|
171
189
|
|
|
190
|
+
{Exhaustive and falsifiable. For coverage/refactor tasks: one criterion per affected call site or pattern, written as a grep/absence assertion. No cap — list must be complete.}
|
|
172
191
|
- [ ] {Specific, testable criterion}
|
|
173
192
|
- [ ] {Specific, testable criterion}
|
|
174
193
|
- [ ] No regression in {related feature}
|
|
@@ -186,6 +205,7 @@ BODY_EOF
|
|
|
186
205
|
|
|
187
206
|
# For satellite repo issues (MCP, n8n) — create in the satellite repo:
|
|
188
207
|
# Note: GitHub milestones are per-repo, so satellite issues reference the milestone by name only
|
|
208
|
+
# Body structure MUST match the Pipeline Issue Template in issue.md Phase 3D.
|
|
189
209
|
gh issue create -R {SATELLITE_REPO} \
|
|
190
210
|
--title "{fix|feat|refactor}: {issue_title}" \
|
|
191
211
|
--label "{type},{priority}" \
|
|
@@ -196,16 +216,17 @@ gh issue create -R {SATELLITE_REPO} \
|
|
|
196
216
|
|
|
197
217
|
## Root Cause (if known)
|
|
198
218
|
|
|
199
|
-
{Why this needs to change. If a new feature: "New capability required for {MILESTONE_TITLE}."}
|
|
219
|
+
{Why this needs to change. Reference specific file:line where possible. If a new feature: "New capability required for {MILESTONE_TITLE}."}
|
|
200
220
|
|
|
201
221
|
## Affected Files
|
|
202
222
|
|
|
203
|
-
Files that need changes:
|
|
223
|
+
Files that need changes (full list from Step 4B code read):
|
|
204
224
|
1. `{filepath}` — {what needs to change}
|
|
205
225
|
2. `{filepath}` — {what needs to change}
|
|
206
226
|
|
|
207
227
|
## Acceptance Criteria
|
|
208
228
|
|
|
229
|
+
{Exhaustive and falsifiable. No cap — coverage/refactor tasks require one criterion per affected call site.}
|
|
209
230
|
- [ ] {Specific, testable criterion}
|
|
210
231
|
- [ ] {Specific, testable criterion}
|
|
211
232
|
|
|
@@ -223,19 +244,47 @@ BODY_EOF
|
|
|
223
244
|
|
|
224
245
|
### Step 6B: Add issues to Project board
|
|
225
246
|
|
|
226
|
-
|
|
247
|
+
**Skip if `project_board` is not configured** — check resolved vars before proceeding:
|
|
227
248
|
|
|
228
249
|
```bash
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
250
|
+
if [ -z "$PROJECT_BOARD_OWNER" ] || [ -z "$PROJECT_ID" ] || [ -z "$PROJECT_NUMBER" ]; then
|
|
251
|
+
echo "INFO: project_board not configured in forge.yaml — skipping board sync for created issues"
|
|
252
|
+
else
|
|
253
|
+
# Resolve component option ID for this repo from forge.yaml → project_board.components
|
|
254
|
+
COMPONENT_OPTION_ID=$(yq '.project_board.components[] | select(.repo == "'"$GH_REPO"'") | .option_id' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
255
|
+
|
|
256
|
+
for ISSUE_NUM in {created_issue_numbers}; do
|
|
257
|
+
ISSUE_URL="https://github.com/$GH_REPO/issues/${ISSUE_NUM}"
|
|
258
|
+
ITEM_ID=$(gh project item-add "$PROJECT_NUMBER" --owner "$PROJECT_BOARD_OWNER" --url "$ISSUE_URL" --format json --jq '.id' 2>/dev/null)
|
|
259
|
+
if [ -n "$ITEM_ID" ]; then
|
|
260
|
+
# Resolve priority option ID from the issue's priority label
|
|
261
|
+
ISSUE_PRIORITY=$(gh issue view "$ISSUE_NUM" "$GH_FLAG" --json labels \
|
|
262
|
+
--jq '[.labels[].name | select(startswith("priority:"))] | .[0] | ltrimstr("priority:") | ascii_downcase' 2>/dev/null || echo "")
|
|
263
|
+
# Validate ISSUE_PRIORITY matches expected pattern before use as yq key path <!-- Added: forge#300 -->
|
|
264
|
+
PRIORITY_OPTION_ID=""
|
|
265
|
+
if [[ "$ISSUE_PRIORITY" =~ ^p[0-3]$ ]]; then
|
|
266
|
+
PRIORITY_OPTION_ID=$(yq '.project_board.option_ids.priority.'"$ISSUE_PRIORITY"' // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
if [ -n "$STATUS_FIELD_ID" ] && [ -n "$STATUS_TODO_OPTION_ID" ]; then
|
|
270
|
+
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
|
|
271
|
+
--field-id "$STATUS_FIELD_ID" --single-select-option-id "$STATUS_TODO_OPTION_ID" 2>/dev/null || true # Status=Todo
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "$LANE_FIELD_ID" ] && [ -n "$LANE_FEATURE_OPTION_ID" ]; then
|
|
274
|
+
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
|
|
275
|
+
--field-id "$LANE_FIELD_ID" --single-select-option-id "$LANE_FEATURE_OPTION_ID" 2>/dev/null || true # Lane=Feature
|
|
276
|
+
fi
|
|
277
|
+
if [ -n "$COMPONENT_FIELD_ID" ] && [ -n "$COMPONENT_OPTION_ID" ]; then
|
|
278
|
+
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
|
|
279
|
+
--field-id "$COMPONENT_FIELD_ID" --single-select-option-id "$COMPONENT_OPTION_ID" 2>/dev/null || true # Component (from forge.yaml → project_board.components)
|
|
280
|
+
fi
|
|
281
|
+
if [ -n "$PRIORITY_FIELD_ID" ] && [ -n "$PRIORITY_OPTION_ID" ]; then
|
|
282
|
+
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
|
|
283
|
+
--field-id "$PRIORITY_FIELD_ID" --single-select-option-id "$PRIORITY_OPTION_ID" 2>/dev/null || true # Priority (from issue label)
|
|
284
|
+
fi
|
|
285
|
+
fi
|
|
286
|
+
done
|
|
287
|
+
fi
|
|
239
288
|
```
|
|
240
289
|
|
|
241
290
|
### Step 7: Report
|
package/commands/orchestrate.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Orchestrate parallel work on multiple issues or an entire milestone — spawns sub-agents that each run the full /work-on pipeline
|
|
3
|
-
argument-hint: [milestone <slug> | #1 #2 #3 | next <N> | fast-lane | P0]
|
|
3
|
+
argument-hint: [milestone <slug> | #1 #2 #3 | next <N> | fast-lane | priority:P0]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /orchestrate — Multi-Issue Parallel Orchestrator
|
|
@@ -37,11 +37,12 @@ GH_FLAG="-R $GH_REPO"
|
|
|
37
37
|
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
38
38
|
PROJECT_NAME=$(yq '.project.name' "$CONFIG_FILE")
|
|
39
39
|
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
40
|
+
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
40
41
|
# Build satellite repo map from repos.satellites list
|
|
41
42
|
# Each satellite: { prefix, repo, staging_branch }
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, and `{
|
|
45
|
+
All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, and `{DEFAULT_BRANCH}` references below are populated from `forge.yaml`.
|
|
45
46
|
|
|
46
47
|
---
|
|
47
48
|
|
|
@@ -70,10 +71,10 @@ Parse `$ARGUMENTS` to determine which issues to work on:
|
|
|
70
71
|
|-------|------------|
|
|
71
72
|
| `milestone <slug>` | All open issues assigned to that GitHub milestone (default repo) |
|
|
72
73
|
| `#1 #2 #3` or `1 2 3` | Specific issue numbers, optionally repo-prefixed (e.g., `#123 mcp:5 n8n:12`) |
|
|
73
|
-
| `next <N>` | Top N priority open issues (P0 first, then P1, etc.) |
|
|
74
|
+
| `next <N>` | Top N priority open issues (priority:P0 first, then priority:P1, etc.) |
|
|
74
75
|
| `next <N> all-repos` | Top N across ALL ecosystem repos |
|
|
75
76
|
| `fast-lane` or `fast` | All open fast-lane issues (no milestone, bugs/fixes) |
|
|
76
|
-
| `P0` or `P1` | All open issues with that priority label |
|
|
77
|
+
| `priority:P0` or `priority:P1` | All open issues with that priority label |
|
|
77
78
|
| `mcp:fast` or `n8n:next 3` | Repo-scoped queries |
|
|
78
79
|
| `<slug>` (no keyword) | Try milestone first, then fall back to label search |
|
|
79
80
|
|
|
@@ -230,6 +231,8 @@ Add the newly created issues to the issue set. Re-check each one:
|
|
|
230
231
|
- Tag them as `IMPLEMENTATION` (investigations don't spawn more investigations)
|
|
231
232
|
- They inherit the milestone from their parent investigation if applicable
|
|
232
233
|
|
|
234
|
+
**Issue body standard**: Any new issues created by investigation agents MUST use the **Pipeline Issue Template** from `issue.md` Phase 3D as their body structure. Investigation agents that produce issues as output should be prompted to use that template — not ad-hoc body formats — so every spawned issue enters the pipeline with the correct structure (Problem, Root Cause, Affected Files, Acceptance Criteria). Verify spawned issues have all mandatory sections before adding them to the batch; if sections are missing, the `/issue` body-validation step (Phase 4C.5) will repair them. <!-- Added: forge#293 -->
|
|
235
|
+
|
|
233
236
|
**Updated issue set** = original `IMPLEMENTATION` issues + newly spawned issues from investigations.
|
|
234
237
|
|
|
235
238
|
### Step 2E: Report Wave 0 results to user
|
|
@@ -275,18 +278,19 @@ For each issue, estimate which domains it touches based on title, body, and labe
|
|
|
275
278
|
for NUM in {issue_numbers}; do
|
|
276
279
|
ISSUE=$(gh issue view $NUM --json title,body,labels --jq '{title: .title, labels: [.labels[].name], body: (.body[:300])}')
|
|
277
280
|
echo "=== #$NUM ==="
|
|
278
|
-
echo "$ISSUE" | grep -qiE "credit|billing|pricing|stripe|
|
|
281
|
+
echo "$ISSUE" | grep -qiE "credit|billing|pricing|stripe|charge|refund" && echo " BILLING" || true
|
|
279
282
|
echo "$ISSUE" | grep -qiE "auth|session|jwt|login|permission|oauth" && echo " AUTH" || true
|
|
280
|
-
echo "$ISSUE" | grep -qiE "
|
|
283
|
+
echo "$ISSUE" | grep -qiE "worker|queue|job|task|background|consumer" && echo " WORKER" || true
|
|
281
284
|
echo "$ISSUE" | grep -qiE "migration|\.sql|database|postgres|alembic" && echo " DATABASE" || true
|
|
282
285
|
echo "$ISSUE" | grep -qiE "component|page|layout|dashboard|ui|ux|frontend|web/src" && echo " FRONTEND" || true
|
|
283
286
|
echo "$ISSUE" | grep -qiE "docker|deploy|traefik|nginx|ci|cd|infra|github.action" && echo " INFRA" || true
|
|
284
|
-
echo "$ISSUE" | grep -qiE "
|
|
287
|
+
echo "$ISSUE" | grep -qiE "llm|extract|schema|format|embedding|model" && echo " AI" || true
|
|
288
|
+
# For project-specific domains, configure keywords in forge.yaml → review.domains and extend above
|
|
285
289
|
done
|
|
286
290
|
```
|
|
287
291
|
|
|
288
292
|
**Use domain info for wave planning:**
|
|
289
|
-
- Issues in the SAME domain (especially
|
|
293
|
+
- Issues in the SAME domain (especially WORKER, BILLING, DATABASE) are more likely to touch the same files → prefer sequential within a wave or adjacent waves
|
|
290
294
|
- Issues in DIFFERENT domains are more likely independent → safe to parallelize
|
|
291
295
|
- BILLING + AUTH issues should be prioritized early (security-critical)
|
|
292
296
|
- **DATABASE issues are ALWAYS serialized — hard rule, no exceptions.** Multiple agents writing migrations simultaneously will produce duplicate migration numbers (e.g., two `0067_*.sql` files), which breaks the migration runner. Every DATABASE issue must be in its own wave. If 3 DATABASE issues are in a batch: Wave 1 runs issue A, Wave 2 runs issue B, Wave 3 runs issue C.
|
|
@@ -363,11 +367,17 @@ When two issues modify different files that **import from the same utility/init
|
|
|
363
367
|
|
|
364
368
|
**Apply inferences:**
|
|
365
369
|
```
|
|
366
|
-
# High-fan-in files — if ANY issue touches these, serialize it with all same-service issues
|
|
370
|
+
# High-fan-in files — if ANY issue touches these, serialize it with all same-service issues.
|
|
371
|
+
# Read layout paths from forge.yaml review.layout; fall back to sensible generic defaults.
|
|
372
|
+
# Example (pseudo-code — adapt to your forge.yaml parsing method):
|
|
373
|
+
# API_MAIN = forge_yaml.review.layout.api_main ?? "services/api/app/main.py"
|
|
374
|
+
# WORKER_MAIN = forge_yaml.review.layout.worker_main ?? "services/worker/worker/main.py"
|
|
375
|
+
# PAGES_ROOT = forge_yaml.review.layout.pages ?? "web/src/app"
|
|
376
|
+
|
|
367
377
|
HIGH_FAN_IN = [
|
|
368
|
-
"services/api/app/main.py"
|
|
369
|
-
"services/worker/worker/
|
|
370
|
-
"web/src/app/layout.tsx"
|
|
378
|
+
API_MAIN, # e.g. "services/api/app/main.py" — router/middleware registration
|
|
379
|
+
WORKER_MAIN, # e.g. "services/worker/worker/main.py" — worker entrypoint (set forge.yaml review.layout.worker_main)
|
|
380
|
+
PAGES_ROOT + "/layout.tsx", # e.g. "web/src/app/layout.tsx" — root layout for all pages
|
|
371
381
|
"docker-compose.yml",
|
|
372
382
|
"docker-compose.prod.yml",
|
|
373
383
|
".env.example"
|
|
@@ -403,7 +413,7 @@ Build the final conflict graph by merging signals from all four layers:
|
|
|
403
413
|
| Layer 4: Low confidence + same domain | **Conservative** | Serialize |
|
|
404
414
|
| Layer 4: Low confidence + no domain | **Conservative** | Serialize with prior wave |
|
|
405
415
|
|
|
406
|
-
**This supplements, not replaces, the domain keyword estimation.** Domain tags still help with broad sequencing decisions. Multi-layer conflict detection catches the specific cases keywords miss (e.g., two issues that both modify files in `services/api/app/models/` where one is labeled
|
|
416
|
+
**This supplements, not replaces, the domain keyword estimation.** Domain tags still help with broad sequencing decisions. Multi-layer conflict detection catches the specific cases keywords miss (e.g., two issues that both modify files in `services/api/app/models/` where one is labeled WORKER and the other BILLING — Layer 2 catches this even though Layer 1 shows no direct file overlap).
|
|
407
417
|
|
|
408
418
|
### Step 3D: Build the execution plan
|
|
409
419
|
|
|
@@ -445,15 +455,15 @@ Wave 2 (after deps): #2634 (enable daemon — after #2633 fixes memory leak), #2
|
|
|
445
455
|
### Domain Distribution
|
|
446
456
|
| Domain | Issues | Notes |
|
|
447
457
|
|--------|--------|-------|
|
|
448
|
-
| SCRAPING | {N} | {High overlap risk / Independent targets} |
|
|
449
458
|
| FRONTEND | {N} | {Independent pages / Shared components} |
|
|
450
459
|
| BILLING | {N} | {Critical — prioritize in Wave 1} |
|
|
451
460
|
| DATABASE | {N} | {Sequential — migration order matters} |
|
|
452
461
|
| AUTH | {N} | {Critical — Wave 1} |
|
|
462
|
+
| WORKER | {N} | {High overlap risk within worker service} |
|
|
463
|
+
| AI | {N} | {Independent} |
|
|
453
464
|
| INFRA | {N} | {Independent} |
|
|
454
|
-
| CORTEX | {N} | {Independent} |
|
|
455
465
|
|
|
456
|
-
(Omit rows with 0 issues.)
|
|
466
|
+
(Omit rows with 0 issues. Add project-specific domain rows from forge.yaml → review.domains.)
|
|
457
467
|
|
|
458
468
|
### Implementation Waves
|
|
459
469
|
|
|
@@ -488,12 +498,18 @@ Proceed? (yes / adjust / pick specific issues)
|
|
|
488
498
|
```bash
|
|
489
499
|
# Capture staging baseline before launching the wave
|
|
490
500
|
git fetch origin
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
501
|
+
if [ "$DEFAULT_BRANCH" = "$STAGING_BRANCH" ]; then
|
|
502
|
+
# Single-branch repo — staging and default are the same; diff is always zero
|
|
503
|
+
STAGING_LINES_BEFORE=0
|
|
504
|
+
echo "Staging baseline before Wave {N}: skipped — single-branch repo (staging == default)"
|
|
505
|
+
else
|
|
506
|
+
STAGING_LINES_BEFORE=$(git diff --stat origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH 2>/dev/null \
|
|
507
|
+
| tail -1 \
|
|
508
|
+
| grep -oP '\d+ insertion' \
|
|
509
|
+
| grep -oP '\d+' \
|
|
510
|
+
|| echo "0")
|
|
511
|
+
echo "Staging baseline before Wave {N}: ${STAGING_LINES_BEFORE} lines ahead of $DEFAULT_BRANCH"
|
|
512
|
+
fi
|
|
497
513
|
```
|
|
498
514
|
|
|
499
515
|
Store `STAGING_LINES_BEFORE` per wave. After the wave completes (after Step 4C), run the integrity check:
|
|
@@ -501,27 +517,32 @@ Store `STAGING_LINES_BEFORE` per wave. After the wave completes (after Step 4C),
|
|
|
501
517
|
```bash
|
|
502
518
|
# Check staging integrity after wave completes (run after Step 4C)
|
|
503
519
|
git fetch origin
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
520
|
+
if [ "$DEFAULT_BRANCH" = "$STAGING_BRANCH" ]; then
|
|
521
|
+
STAGING_LINES_AFTER=0
|
|
522
|
+
STAGING_GROWTH=0
|
|
523
|
+
echo "Staging integrity check: skipped — single-branch repo (staging == default)"
|
|
524
|
+
else
|
|
525
|
+
STAGING_LINES_AFTER=$(git diff --stat origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH 2>/dev/null \
|
|
526
|
+
| tail -1 \
|
|
527
|
+
| grep -oP '\d+ insertion' \
|
|
528
|
+
| grep -oP '\d+' \
|
|
529
|
+
|| echo "0")
|
|
530
|
+
STAGING_GROWTH=$((STAGING_LINES_AFTER - STAGING_LINES_BEFORE))
|
|
531
|
+
echo "Staging after Wave {N}: ${STAGING_LINES_AFTER} lines (+${STAGING_GROWTH} vs baseline)"
|
|
532
|
+
|
|
533
|
+
# Alert if growth exceeds expected delta
|
|
534
|
+
# Expected: only the lines from PRs merged in this wave
|
|
535
|
+
# Alert threshold: growth > 500 lines more than the sum of changed lines from wave PRs
|
|
536
|
+
# (Use 0 as threshold if no PRs merged — any growth is unexpected)
|
|
537
|
+
EXPECTED_DELTA={SUM_OF_PR_LINE_COUNTS_THIS_WAVE} # set from PR diffs collected in 4B
|
|
538
|
+
UNEXPECTED_GROWTH=$((STAGING_GROWTH - EXPECTED_DELTA))
|
|
539
|
+
if [ "$UNEXPECTED_GROWTH" -gt 500 ]; then
|
|
540
|
+
echo "ALERT: Staging grew by ${STAGING_GROWTH} lines (+${UNEXPECTED_GROWTH} beyond expected ${EXPECTED_DELTA})."
|
|
541
|
+
echo "This may indicate milestone-code contamination via agent merge commits."
|
|
542
|
+
echo "Review: git log --oneline --merges origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH"
|
|
543
|
+
echo "Do NOT merge $STAGING_BRANCH → $DEFAULT_BRANCH until the unexpected growth is investigated."
|
|
544
|
+
# Do NOT auto-stop — alert the user and let them decide
|
|
545
|
+
fi
|
|
525
546
|
fi
|
|
526
547
|
```
|
|
527
548
|
|
|
@@ -738,7 +759,7 @@ for FINDING_NUM in {spawned_finding_numbers}; do
|
|
|
738
759
|
FINDING_DATA=$(gh issue view $FINDING_NUM -R {GH_REPO} --json labels,title,body \
|
|
739
760
|
--jq '{labels: [.labels[].name], title: .title, body: .body}')
|
|
740
761
|
|
|
741
|
-
PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("P")) |
|
|
762
|
+
PRIORITY=$(echo "$FINDING_DATA" | jq -r '.labels[] | select(startswith("priority:P")) | ltrimstr("priority:")' | head -1)
|
|
742
763
|
TITLE=$(echo "$FINDING_DATA" | jq -r '.title')
|
|
743
764
|
|
|
744
765
|
# Heuristic 1: Generation check — source issue has review-finding label (always defer, even for P1/P2)
|
|
@@ -799,7 +820,14 @@ They remain open in GitHub — they will be picked up by future `/orchestrate` o
|
|
|
799
820
|
|
|
800
821
|
**When to run**: After each wave completes, IF the batch targets a milestone branch AND any `.tsx`/`.ts` files were changed by agents in the completed wave.
|
|
801
822
|
|
|
823
|
+
All tool commands are read from `forge.yaml → verification.commands`; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing.
|
|
824
|
+
|
|
802
825
|
```bash
|
|
826
|
+
# Read toolchain commands from forge.yaml
|
|
827
|
+
TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
|
|
828
|
+
TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
|
|
829
|
+
PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
|
|
830
|
+
|
|
803
831
|
# Check if this is a milestone batch with TypeScript changes
|
|
804
832
|
MILESTONE_BRANCH="milestone/{milestone_slug}"
|
|
805
833
|
TS_CHANGED=$(git diff origin/{DEFAULT_BRANCH}...origin/${MILESTONE_BRANCH} --name-only | grep -E '\.(tsx?|jsx?)$' | head -1)
|
|
@@ -809,20 +837,31 @@ if [ -n "$TS_CHANGED" ]; then
|
|
|
809
837
|
cd {REPO_PATH}
|
|
810
838
|
git fetch origin ${MILESTONE_BRANCH}
|
|
811
839
|
git checkout origin/${MILESTONE_BRANCH} --detach 2>/dev/null
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
840
|
+
|
|
841
|
+
if [ -n "$TS_TYPECHECK" ]; then
|
|
842
|
+
eval "$TS_TYPECHECK" 2>&1 | head -30
|
|
843
|
+
TSC_EXIT=$?
|
|
844
|
+
else
|
|
845
|
+
echo "SKIPPED — typescript.typecheck not configured in verification.commands"
|
|
846
|
+
TSC_EXIT=0
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
if [ "$TSC_EXIT" -eq 0 ] && [ -n "$TS_BUILD" ]; then
|
|
850
|
+
eval "$TS_BUILD" 2>&1 | tail -30
|
|
816
851
|
BUILD_EXIT=$?
|
|
852
|
+
elif [ -z "$TS_BUILD" ]; then
|
|
853
|
+
echo "SKIPPED — typescript.build not configured in verification.commands"
|
|
854
|
+
BUILD_EXIT=0
|
|
817
855
|
fi
|
|
818
|
-
|
|
856
|
+
|
|
857
|
+
git checkout - 2>/dev/null
|
|
819
858
|
|
|
820
859
|
if [ "$TSC_EXIT" -ne 0 ]; then
|
|
821
860
|
echo "BLOCKING: TypeScript errors on ${MILESTONE_BRANCH} after Wave {N}."
|
|
822
861
|
echo "Fix type errors before starting next wave."
|
|
823
862
|
elif [ "${BUILD_EXIT:-0}" -ne 0 ]; then
|
|
824
|
-
echo "BLOCKING:
|
|
825
|
-
echo "
|
|
863
|
+
echo "BLOCKING: build failed on ${MILESTONE_BRANCH} after Wave {N}."
|
|
864
|
+
echo "Build/prerender errors — fix before starting next wave."
|
|
826
865
|
fi
|
|
827
866
|
fi
|
|
828
867
|
|
|
@@ -832,20 +871,25 @@ if [ -n "$PY_CHANGED" ]; then
|
|
|
832
871
|
echo "=== Integration Build Gate (Python): Wave {N} ==="
|
|
833
872
|
cd {REPO_PATH}
|
|
834
873
|
git checkout origin/${MILESTONE_BRANCH} --detach 2>/dev/null
|
|
835
|
-
cd services/api && poetry run black --check app/ 2>&1 | tail -10
|
|
836
|
-
BLACK_EXIT=$?
|
|
837
|
-
poetry run isort --check app/ 2>&1 | tail -10
|
|
838
|
-
ISORT_EXIT=$?
|
|
839
|
-
cd ../.. && git checkout - 2>/dev/null
|
|
840
874
|
|
|
841
|
-
if [
|
|
875
|
+
if [ -n "$PYTHON_FORMAT" ]; then
|
|
876
|
+
eval "$PYTHON_FORMAT" 2>&1 | tail -10
|
|
877
|
+
FORMAT_EXIT=$?
|
|
878
|
+
else
|
|
879
|
+
echo "SKIPPED — python.format not configured in verification.commands"
|
|
880
|
+
FORMAT_EXIT=0
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
git checkout - 2>/dev/null
|
|
884
|
+
|
|
885
|
+
if [ "$FORMAT_EXIT" -ne 0 ]; then
|
|
842
886
|
echo "WARNING: Python formatting issues on ${MILESTONE_BRANCH} after Wave {N}."
|
|
843
887
|
echo "Not blocking but should be fixed before milestone→staging."
|
|
844
888
|
fi
|
|
845
889
|
fi
|
|
846
890
|
```
|
|
847
891
|
|
|
848
|
-
**If the gate fails**: Report the errors to the user. Do NOT proceed to the next wave. The accumulated milestone branch has integration errors that will only get worse with more PRs on top.
|
|
892
|
+
**If the gate fails**: Report the errors to the user. Do NOT proceed to the next wave. The accumulated milestone branch has integration errors that will only get worse with more PRs on top. Build failures are BLOCKING — SSG/prerender crashes are invisible to typecheck alone — configure `typescript.build` in `verification.commands` to catch them.
|
|
849
893
|
|
|
850
894
|
### Step 4E: Advance to next wave
|
|
851
895
|
|
|
@@ -1051,12 +1095,12 @@ Aggregate into the batch-level analytics for Step 6B.
|
|
|
1051
1095
|
→ Gets all unmilestoned bugs/fixes
|
|
1052
1096
|
→ Groups by independence, runs in waves
|
|
1053
1097
|
|
|
1054
|
-
### "orchestrate milestone
|
|
1055
|
-
→ Fetches all open issues in "
|
|
1056
|
-
→ Detects #
|
|
1057
|
-
→ Wave 0: Runs `/work-on
|
|
1058
|
-
→ Re-fetches milestone issues, now includes #
|
|
1059
|
-
→ Wave 1: #
|
|
1060
|
-
→ Wave 2: #
|
|
1098
|
+
### "orchestrate milestone user-auth-v2"
|
|
1099
|
+
→ Fetches all open issues in "User Auth v2" milestone
|
|
1100
|
+
→ Detects #42 is an investigation issue (title: "Investigate: session expiry race condition under high concurrency")
|
|
1101
|
+
→ Wave 0: Runs `/work-on 42` — investigation creates 3 new issues (#55-#57)
|
|
1102
|
+
→ Re-fetches milestone issues, now includes #55-#57
|
|
1103
|
+
→ Wave 1: #38 (P0 token refresh bug), #41 (session store migration), #55, #56 (all independent)
|
|
1104
|
+
→ Wave 2: #39 (enable refresh rotation — after #38 fixes token bug), #57 (depends on #55)
|
|
1061
1105
|
→ Wave 3: remaining issues
|
|
1062
1106
|
→ Final report shows investigation spawned issues and their results
|
|
@@ -15,7 +15,14 @@ Read project identity from `forge.yaml` before running any phase:
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
18
|
-
|
|
18
|
+
GH_REPO=$(yq e '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
19
|
+
GH_FLAG="-R $GH_REPO"
|
|
20
|
+
# FORGE_REPO: the self-pipeline repo where pipeline-health issues are filed.
|
|
21
|
+
# Set project.forge_repo in forge.yaml if your pipeline repo differs from GH_REPO.
|
|
22
|
+
# Example: project:
|
|
23
|
+
# forge_repo: "my-org/my-forge"
|
|
24
|
+
FORGE_REPO=$(yq '.project.forge_repo // ""' "$CONFIG_FILE")
|
|
25
|
+
[ -z "$FORGE_REPO" ] && FORGE_REPO="$GH_REPO"
|
|
19
26
|
FORGE_HOME=$(yq e '.paths.root' "$CONFIG_FILE")
|
|
20
27
|
echo "Forge repo: $FORGE_REPO"
|
|
21
28
|
echo "Forge home: $FORGE_HOME"
|
package/commands/qa-sweep.md
CHANGED
|
@@ -170,8 +170,19 @@ fi
|
|
|
170
170
|
**Dynamic discovery from filesystem** (NOT hardcoded):
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
# Read pages root from forge.yaml review.layout.pages, fall back to web/src/app
|
|
174
|
+
PAGES_ROOT=$(yq '.review.layout.pages // "web/src/app"' $PROJECT_ROOT/forge.yaml 2>/dev/null || echo "web/src/app")
|
|
175
|
+
|
|
176
|
+
cd $PROJECT_ROOT/$PAGES_ROOT
|
|
177
|
+
PAGES=$(find . -name "page.tsx" -o -name "page.js" | sort)
|
|
178
|
+
|
|
179
|
+
# Loud failure when zero pages found — prevents silent no-op qa-sweep runs
|
|
180
|
+
if [ -z "$PAGES" ]; then
|
|
181
|
+
echo "ERROR: Zero pages discovered under $PAGES_ROOT. If your project uses a different layout, set review.layout.pages in forge.yaml (e.g., 'apps/web/src/app' or 'src/pages')."
|
|
182
|
+
exit 1
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo "$PAGES"
|
|
175
186
|
```
|
|
176
187
|
|
|
177
188
|
Convert paths to routes: `./dashboard/billing/page.tsx` → `/dashboard/billing`
|
|
@@ -180,7 +191,7 @@ Convert paths to routes: `./dashboard/billing/page.tsx` → `/dashboard/billing`
|
|
|
180
191
|
|
|
181
192
|
**Resolve dynamic routes** (`[param]`) by fetching real IDs from API. Test invalid IDs too.
|
|
182
193
|
|
|
183
|
-
**For `diff` mode**: `git diff --name-only origin/main...origin/staging --
|
|
194
|
+
**For `diff` mode**: `git diff --name-only origin/main...origin/staging -- $PAGES_ROOT/` → map to affected pages.
|
|
184
195
|
|
|
185
196
|
**Present test plan to user** (page count by category, journey count, estimated waves). Wait for confirmation.
|
|
186
197
|
|
|
@@ -987,7 +987,7 @@ done
|
|
|
987
987
|
gh pr comment [PR_NUMBER] --body "$(cat <<'EOF'
|
|
988
988
|
## Scraper Logic Audit
|
|
989
989
|
|
|
990
|
-
### Components Affected: [
|
|
990
|
+
### Components Affected: [list the specific service/worker/api components touched — e.g. job-queue/rate-limiter/validation/gate-semantics/cross-component-gate]
|
|
991
991
|
|
|
992
992
|
### Tier Flow Analysis
|
|
993
993
|
[Trace the tier selection and escalation with file:line references]
|