forgedock 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,37 +1,33 @@
1
1
  ---
2
- description: Autonomous platform improvement cycle — recon, triage, fix, report. Runs recon+triage by default; pass --fix to also pick up and fix top issues. Human gates all deploys.
3
- argument-hint: [--fix | --recon-only | --fix --limit 5 | --dry-run | --fix --yes]
2
+ description: Autonomous deploy loop — runs until zero open issues remain. Detects pipeline state and resumes from any position. Fully autonomous after invocation.
3
+ argument-hint: [--dry-run | --recon-only]
4
4
  install: extras
5
5
  ---
6
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
7
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
6
8
 
7
- # /autopilot — Recursive Platform Improvement Cycle
9
+ # /autopilot — Autonomous Deploy Loop
8
10
 
9
- **Input**: $ARGUMENTS (default: recon + triage + report, no fixing)
11
+ **Input**: $ARGUMENTS (default: full autonomous loop until zero open issues remain)
10
12
 
11
13
  **Config variables used by this command** (set in `forge.yaml`):
12
14
  - `{CREDENTIALS_FILE}` ← `paths.credentials.file` (optional) — path to credentials YAML for analytics APIs
13
- - `{SERVER_SSH}` ← `services.server_ssh` (optional) — SSH target for production server health checks (e.g., `ubuntu@1.2.3.4`)
14
- - `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to a directory of open work-item files (e.g. `/srv/ops/inbox`)
15
- - `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data collection in the Analytics Snapshot agent
16
-
17
- Resolve `{BILLING_ENABLED}` from `forge.yaml` before executing any phase:
18
-
19
- ```bash
20
- BILLING_ENABLED=$(yq '.billing.enabled // false' "$(git rev-parse --show-toplevel)/forge.yaml" 2>/dev/null || echo 'false')
21
- ```
15
+ - `{SERVER_SSH}` ← `services.server_ssh` (optional) — SSH target for production server health checks
16
+ - `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to ops work-item files
17
+ - `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data in Analytics Snapshot
22
18
 
23
19
  **NEVER use plan mode (EnterPlanMode).**
24
- **NEVER use the Agent tool** — autopilot dispatches work via `Skill(skill="work-on", ...)` only. The Agent tool bypasses the Skill pipeline's label state machine, investigation comments, and structured review — leaving no audit trail.
20
+ **NEVER use the Agent tool** — autopilot dispatches all work via `Skill(...)` calls only. The Agent tool bypasses the Skill pipeline's label state machine, investigation comments, and structured review — leaving no audit trail.
25
21
 
26
22
  <!-- FORGE:SPEC_LOADED — autopilot.md loaded and active. Agent is bound by this spec. -->
27
23
 
28
- You are an autonomous improvement engine for this project. Your job is to **find what's wrong, create trackable issues, and optionally fix the highest-impact ones** — all in a single cycle. Every cycle leaves the platform measurably better than before.
24
+ You are a fully autonomous deploy loop for this project. Your job is to **detect the current pipeline state, work through all open issues from highest to lowest priority, and deploy everything — without stopping for user confirmation**. Invoking `/autopilot` is the authorization to run to completion.
29
25
 
30
- **This is designed to run repeatedly.** Each cycle builds on the last — issues created in cycle N get fixed in cycle N+1. The platform compounds improvements over time.
26
+ **This command overrides the standard "never merge to main" rule.** `/autopilot` IS the authorized deploy system. It ships staging→main and milestone→staging→main as part of normal operation.
31
27
 
32
- **NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
28
+ **This command resumes from wherever the pipeline is stuck.** It always reads current GitHub state before taking any action.
33
29
 
34
- **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Pass model explicitly in every `Agent`/`Task` tool call. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
30
+ **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`.
35
31
 
36
32
  ---
37
33
 
@@ -39,562 +35,651 @@ You are an autonomous improvement engine for this project. Your job is to **find
39
35
 
40
36
  | Flag | Effect |
41
37
  |------|--------|
42
- | (none) | Phases 1-3 only (recon, triage, report) |
43
- | `--fix` | Also run Phase 4 (pick top issues, fix via /work-on) |
44
- | `--fix --limit N` | Fix at most N issues (default: 3) |
45
- | `--recon-only` | Phase 1 only (data collection, no issue creation) |
46
- | `--dry-run` | Run everything but don't create issues or PRs — just report what would happen |
47
- | `--fix --yes` | Auto-approve low-risk fixes (P3, single-file, non-sensitive domain); P0/P1, multi-file, and sensitive-domain fixes still require confirmation |
48
-
49
- Parse `$ARGUMENTS` and set these variables:
50
- ```
51
- MODE = "full" | "recon-only" | "fix" # derived from flags
52
- DO_FIX = true if --fix present
53
- FIX_LIMIT = N from --limit N, default 3
54
- DRY_RUN = true if --dry-run present
55
- AUTO_YES = true if --yes present
38
+ | (none) | Full autonomous loop — state detection → recon → fast lane → milestone → report |
39
+ | `--dry-run` | Run all phases but do NOT create issues, merge PRs, or modify state — report only |
40
+ | `--recon-only` | Phase 0 (state detection) and Phase 1 (recon) only — no loop execution |
41
+
42
+ Parse `$ARGUMENTS` and set:
43
+ ```bash
44
+ DRY_RUN=false
45
+ RECON_ONLY=false
46
+
47
+ for arg in $ARGUMENTS; do
48
+ case "$arg" in
49
+ --dry-run) DRY_RUN=true ;;
50
+ --recon-only) RECON_ONLY=true ;;
51
+ esac
52
+ done
56
53
  ```
57
54
 
58
55
  ---
59
56
 
60
- ## Phase 0: Cycle Context
61
-
62
- **Goal**: Understand what happened since the last cycle.
57
+ ## Config Preamble (MANDATORY — run before any phase)
63
58
 
64
- ### 0A: Timestamp & baseline
59
+ Read all `forge.yaml` config variables before any logic runs:
65
60
 
66
61
  ```bash
67
- echo "=== Autopilot Cycle: $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
62
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
63
+ if [ ! -f "$CONFIG_FILE" ]; then
64
+ echo "ERROR: forge.yaml not found. Run: npx forgedock init"
65
+ exit 1
66
+ fi
68
67
 
69
- # Portable ISO-8601 date arithmetic using python3 (works on Windows, macOS, Linux).
70
- # Replaces GNU-only `date -u -d 'N days ago'` which fails on Windows and macOS.
71
- DATE_3D_AGO=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(days=3)).strftime('%Y-%m-%dT%H:%M:%SZ'))")
72
- DATE_7D_AGO=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%SZ'))")
73
- DATE_1D_AGO=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ'))")
68
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE" 2>/dev/null)
69
+ GH_FLAG="-R $GH_REPO"
70
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE" 2>/dev/null || git rev-parse --show-toplevel)
71
+ STAGING_BRANCH=$(yq '.branches.staging // "staging"' "$CONFIG_FILE" 2>/dev/null || echo "staging")
72
+ DEFAULT_BRANCH=$(yq '.branches.default // "main"' "$CONFIG_FILE" 2>/dev/null || echo "main")
74
73
 
75
- # Recent closed issues (last 3 days) — what was fixed recently?
76
- gh issue list --state closed --json number,title,labels,closedAt \
77
- --jq '[.[] | select(.closedAt > "'"$DATE_3D_AGO"'")] | length' 2>/dev/null || echo "0"
74
+ # Optional config — gracefully absent
75
+ CREDENTIALS_FILE=$(yq '.paths.credentials.file // ""' "$CONFIG_FILE" 2>/dev/null || echo '')
76
+ SERVER_SSH=$(yq '.services.server_ssh // ""' "$CONFIG_FILE" 2>/dev/null || echo '')
77
+ OPS_INBOX_PATH=$(yq '.services.ops_inbox_path // ""' "$CONFIG_FILE" 2>/dev/null || echo '')
78
+ BILLING_ENABLED=$(yq '.billing.enabled // false' "$CONFIG_FILE" 2>/dev/null || echo 'false')
78
79
 
79
- # Open issue count by priority
80
- gh issue list --state open --label "priority:P0" --json number --jq 'length'
81
- gh issue list --state open --label "priority:P1" --json number --jq 'length'
82
- gh issue list --state open --label "priority:P2" --json number --jq 'length'
80
+ echo "Autopilot: repo=$GH_REPO staging=$STAGING_BRANCH default=$DEFAULT_BRANCH dry_run=$DRY_RUN recon_only=$RECON_ONLY"
81
+ ```
83
82
 
84
- # Stale issues (open, no workflow label, older than 7 days)
85
- gh issue list --state open --limit 200 --json number,title,labels,createdAt \
86
- --jq '[.[] | select(
87
- (.labels | map(.name) | any(startswith("workflow:")) | not) and
88
- (.createdAt < "'"$DATE_7D_AGO"'")
89
- )] | length'
83
+ ---
90
84
 
91
- # Failed CI runs in last 24h
92
- gh run list --limit 30 --json conclusion,createdAt \
93
- --jq '[.[] | select(.conclusion == "failure" and .createdAt > "'"$DATE_1D_AGO"'")] | length'
94
- ```
85
+ ## Phase 0: State Detection
95
86
 
96
- Store these as `BASELINE` metrics for the cycle report.
87
+ **Goal**: Read current GitHub state and determine where the pipeline is. Always start here — never assume clean state.
97
88
 
98
- ### 0B: Check for open P0s
89
+ ### 0A: Detect open staging→main PR
99
90
 
100
91
  ```bash
101
- gh issue list --state open --label "priority:P0" --json number,title --jq '.[] | "#\(.number) \(.title)"'
92
+ STAGING_TO_MAIN_PR=$(gh pr list $GH_FLAG \
93
+ --head "$STAGING_BRANCH" \
94
+ --base "$DEFAULT_BRANCH" \
95
+ --state open \
96
+ --json number,title,headRefOid \
97
+ --jq '.[0] // empty' 2>/dev/null)
98
+
99
+ if [ -n "$STAGING_TO_MAIN_PR" ]; then
100
+ STAGING_PR_NUMBER=$(echo "$STAGING_TO_MAIN_PR" | jq -r '.number')
101
+ echo "STATE: Open staging→main PR #$STAGING_PR_NUMBER detected"
102
+ else
103
+ STAGING_PR_NUMBER=""
104
+ echo "STATE: No open staging→main PR"
105
+ fi
102
106
  ```
103
107
 
104
- If any P0 issues exist:
105
- - **STOP the normal cycle**
106
- - Print: `P0 issue(s) open — autopilot prioritizing these above all else`
107
- - Set `FIX_TARGETS` to the P0 issue numbers
108
- - Skip to Phase 4 (fix mode, regardless of flags)
108
+ ### 0B: Detect open milestone→staging PRs
109
109
 
110
- ---
110
+ ```bash
111
+ MILESTONE_PRS=$(gh pr list $GH_FLAG \
112
+ --base "$STAGING_BRANCH" \
113
+ --state open \
114
+ --json number,title,headRefName \
115
+ --jq '[.[] | select(.headRefName | startswith("milestone/"))]' 2>/dev/null || echo '[]')
111
116
 
112
- ## Phase 1: Recon (Parallel Data Collection)
117
+ MILESTONE_PR_COUNT=$(echo "$MILESTONE_PRS" | jq 'length' 2>/dev/null || echo '0')
118
+ echo "STATE: $MILESTONE_PR_COUNT open milestone→staging PR(s)"
119
+ ```
113
120
 
114
- **Goal**: Pull signals from every available source. Launch ALL collectors simultaneously.
121
+ ### 0C: Detect in-flight issues (stuck in intermediate workflow states)
115
122
 
116
- ### 1A: Launch parallel recon agents
123
+ ```bash
124
+ INFLIGHT_ISSUES=$(gh issue list $GH_FLAG \
125
+ --state open \
126
+ --limit 200 \
127
+ --json number,title,labels \
128
+ --jq '[.[] | select(.labels | map(.name) | any(. == "workflow:building" or . == "workflow:in-review"))] | length' \
129
+ 2>/dev/null || echo '0')
130
+ echo "STATE: $INFLIGHT_ISSUES in-flight issue(s) (workflow:building or workflow:in-review)"
131
+ ```
117
132
 
118
- Spawn these as **background agents** (all `run_in_background=true`, all `model="sonnet"`):
133
+ ### 0D: Detect staging vs main delta
119
134
 
120
- **Agent: Production Health**
121
- ```
122
- Check production system health:
123
- 1. SSH to check for open ops-inbox items: if {SERVER_SSH} is configured, run: ssh {SERVER_SSH} "ls {OPS_INBOX_PATH}/*-open-* 2>/dev/null" — skip this step if SERVER_SSH or OPS_INBOX_PATH is not set in forge.yaml
124
- 2. If any exist, cat each one and summarize
125
- 3. Check container health via MCP: get_production_status, run_production_health_check
126
- 4. Check recent error logs: get_production_logs for api and worker (last 100 lines), grep for ERROR/CRITICAL
127
- 5. Return: memo summaries, unhealthy containers, error patterns with counts
135
+ ```bash
136
+ git fetch origin "$STAGING_BRANCH" "$DEFAULT_BRANCH" 2>/dev/null || true
137
+ STAGING_AHEAD=$(git rev-list --count "origin/${DEFAULT_BRANCH}..origin/${STAGING_BRANCH}" 2>/dev/null || echo '0')
138
+ echo "STATE: staging is $STAGING_AHEAD commit(s) ahead of $DEFAULT_BRANCH"
128
139
  ```
129
140
 
130
- **Agent: Scraping Intelligence**
131
- ```
132
- Pull scraping performance data:
133
- 1. Read scrape_diagnostics summary from admin API or production logs
134
- 2. Check Redis for tier feedback stats: tier success/fail rates across domains
135
- 3. Look for patterns: domains with >50% failure rate, tiers with degraded performance
136
- 4. Return: top failing domains (count + tier), tier pass rates, any new anti-bot patterns
137
- ```
141
+ ### 0E: Count open unmilestoned issues
138
142
 
139
- **Agent: CI/CD Health**
140
- ```
141
- Check CI pipeline health:
142
- 1. gh run list --limit 30 — count failures by workflow name
143
- 2. For each failed run: gh run view {id} --json jobs — identify which job failed
144
- 3. Check if ecosystem-sync is consistently failing (known issue vs new)
145
- 4. Return: failure rate, recurring failures, new failures
143
+ ```bash
144
+ OPEN_FAST_LANE=$(gh issue list $GH_FLAG \
145
+ --state open \
146
+ --limit 200 \
147
+ --json number,title,milestone,labels \
148
+ --jq '[.[] | select(
149
+ .milestone == null and
150
+ (.labels | map(.name) | any(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:decomposed") | not)
151
+ )] | length' \
152
+ 2>/dev/null || echo '0')
153
+ echo "STATE: $OPEN_FAST_LANE open unmilestoned issue(s) for fast lane"
146
154
  ```
147
155
 
148
- **Agent: Issue Backlog Health**
149
- ```
150
- Analyze GitHub issue backlog:
151
- 1. gh issue list --state open --limit 200 — full list with labels, milestones, dates
152
- 2. Count by: priority (P0-P3), type (bug/feature/etc), milestone, age
153
- 3. Find stale issues: open > 14 days, no workflow label, no recent comments
154
- 4. Find orphaned issues: workflow:in-review but no open PR
155
- 5. Find blocked issues: depends on another open issue
156
- 6. Return: backlog summary, stale issues list, orphans, blockers
157
- ```
156
+ ### 0F: Summarize detected state
158
157
 
159
- **Agent: Analytics Snapshot** (lightweight — not the full /analytics audit)
160
158
  ```
161
- Quick analytics pulse — just the key metrics, not a full audit:
162
- 1. Read credentials from {CREDENTIALS_FILE} (set via paths.credentials.file in forge.yaml)
163
- 2. GSC: mcp__gsc__search_analytics for last 7 days — total clicks, impressions, avg position
164
- 3. Stripe (only if {BILLING_ENABLED} is true): mcp__stripe__retrieve_balance — current balance.
165
- Skip this step entirely if {BILLING_ENABLED} is false — do NOT call any Stripe MCP tools.
166
- 4. Return: clicks trend (up/down), revenue (or "N/A — billing.enabled: false" if skipped), notable changes
159
+ ## Current Pipeline State
160
+
161
+ | Signal | Value |
162
+ |--------|-------|
163
+ | staging→main PR open | ${STAGING_PR_NUMBER:-none} |
164
+ | milestone→staging PRs open | $MILESTONE_PR_COUNT |
165
+ | In-flight issues (stuck) | $INFLIGHT_ISSUES |
166
+ | staging commits ahead of $DEFAULT_BRANCH | $STAGING_AHEAD |
167
+ | Open fast-lane issues | $OPEN_FAST_LANE |
167
168
  ```
168
169
 
169
- ### 1B: Collect results
170
+ **Resume logic**:
171
+ - If `INFLIGHT_ISSUES > 0` → call `/recover-orphans` first in Phase 1A before running recon
172
+ - If `STAGING_PR_NUMBER` is set → that deploy is in progress; `/deploy-pr` will detect and resume it
173
+ - Otherwise → proceed through phases in order
170
174
 
171
- Wait for all agents to complete. Aggregate into a `RECON_DATA` object:
172
- ```
173
- RECON_DATA = {
174
- production: { memos, unhealthy_containers, error_patterns },
175
- scraping: { failing_domains, tier_rates, new_patterns },
176
- ci: { failure_rate, recurring_failures, new_failures },
177
- backlog: { summary, stale_issues, orphans, blockers },
178
- analytics: { clicks_trend, revenue, notable_changes }
179
- }
175
+ ---
176
+
177
+ ## Phase 1: Recon
178
+
179
+ **Goal**: Surface signals that need new GitHub issues. Lightweight — CI health, issue backlog, optional analytics pulse.
180
+
181
+ If `RECON_ONLY=true`, print the recon report and **stop after this phase**.
182
+
183
+ ### 1A: Recover orphaned pipeline state (MANDATORY — run before recon)
184
+
185
+ Orphaned issues (stuck in workflow:building or workflow:in-review without an active agent) cause loop contamination. Recover them first so the loop sees accurate open issue counts.
186
+
187
+ ```bash
188
+ RECOVER_ORPHANS_AVAILABLE=$(ls ~/.claude/commands/recover-orphans.md 2>/dev/null && echo "true" || echo "false")
189
+
190
+ if [ "$INFLIGHT_ISSUES" -gt 0 ]; then
191
+ if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ] && [ "$DRY_RUN" = "false" ]; then
192
+ echo "Recovering $INFLIGHT_ISSUES orphaned pipeline issue(s)..."
193
+ Skill("recover-orphans", args="")
194
+ elif [ "$DRY_RUN" = "true" ]; then
195
+ echo "[DRY-RUN] Would invoke: Skill(recover-orphans)"
196
+ else
197
+ echo "WARNING: /recover-orphans not installed — cannot auto-recover orphaned issues. Install it first."
198
+ fi
199
+ else
200
+ echo "No in-flight issues — skipping orphan recovery"
201
+ fi
180
202
  ```
181
203
 
182
- If `MODE == "recon-only"`, print RECON_DATA as a formatted report and **stop here**.
204
+ ### 1B: CI/CD Health
183
205
 
184
- ---
206
+ ```bash
207
+ DATE_1D_AGO=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ'))")
208
+
209
+ RECENT_FAILURES=$(gh run list $GH_FLAG --limit 30 --json conclusion,createdAt,workflowName \
210
+ --jq "[.[] | select(.conclusion == \"failure\" and .createdAt > \"$DATE_1D_AGO\")] | length" \
211
+ 2>/dev/null || echo "0")
185
212
 
186
- ## Phase 2: Triage & Issue Creation
213
+ RECENT_RUNS=$(gh run list $GH_FLAG --limit 30 --json conclusion,createdAt \
214
+ --jq "[.[] | select(.createdAt > \"$DATE_1D_AGO\")] | length" \
215
+ 2>/dev/null || echo "0")
187
216
 
188
- **Goal**: Convert recon signals into actionable, deduplicated GitHub issues.
217
+ echo "CI (24h): $RECENT_FAILURES/$RECENT_RUNS failures"
189
218
 
190
- ### 2A: Deduplication check
219
+ # Recurring failures (same workflow failing multiple times)
220
+ RECURRING=$(gh run list $GH_FLAG --limit 30 --json conclusion,workflowName \
221
+ --jq '[.[] | select(.conclusion == "failure")] | group_by(.workflowName) | .[] | select(length > 1) | "\(length)x \(.[0].workflowName)"' \
222
+ 2>/dev/null || echo '')
223
+ [ -n "$RECURRING" ] && echo "Recurring CI failures: $RECURRING"
224
+ ```
191
225
 
192
- Before creating ANY issue, search for existing open issues that cover the same problem:
226
+ ### 1C: Issue Backlog Health
193
227
 
194
228
  ```bash
195
- # For each finding, search existing issues
196
- gh issue list --state open --limit 200 --json number,title,labels \
197
- --jq '.[] | "\(.number) \(.title)"'
229
+ DATE_14D_AGO=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) - timedelta(days=14)).strftime('%Y-%m-%dT%H:%M:%SZ'))")
230
+
231
+ # Count by priority
232
+ P0_COUNT=$(gh issue list $GH_FLAG --state open --limit 200 --json labels \
233
+ --jq '[.[] | select(.labels | map(.name) | any(. == "P0"))] | length' 2>/dev/null || echo "0")
234
+ P1_COUNT=$(gh issue list $GH_FLAG --state open --limit 200 --json labels \
235
+ --jq '[.[] | select(.labels | map(.name) | any(. == "P1"))] | length' 2>/dev/null || echo "0")
236
+ P2_COUNT=$(gh issue list $GH_FLAG --state open --limit 200 --json labels \
237
+ --jq '[.[] | select(.labels | map(.name) | any(. == "P2"))] | length' 2>/dev/null || echo "0")
238
+
239
+ # Stale issues (open >14d, no workflow label, no milestone)
240
+ STALE_COUNT=$(gh issue list $GH_FLAG --state open --limit 200 --json number,labels,createdAt,milestone \
241
+ --jq "[.[] | select(
242
+ (.labels | map(.name) | any(startswith(\"workflow:\")) | not) and
243
+ (.createdAt < \"$DATE_14D_AGO\") and
244
+ .milestone == null
245
+ )] | length" 2>/dev/null || echo "0")
246
+
247
+ echo "Backlog: P0=$P0_COUNT P1=$P1_COUNT P2=$P2_COUNT stale=$STALE_COUNT"
198
248
  ```
199
249
 
200
- **Rule**: If an existing open issue covers the finding (even partially), DO NOT create a duplicate. Instead, add a comment to the existing issue with the new data point.
250
+ ### 1D: Analytics Pulse (optional — forge.yaml-gated)
201
251
 
202
- ### 2B: Classify and prioritize findings
252
+ ```bash
253
+ # Only run if CREDENTIALS_FILE is configured and the file exists
254
+ if [ -n "$CREDENTIALS_FILE" ] && [ -f "$CREDENTIALS_FILE" ]; then
255
+ echo "Analytics pulse: credentials at $CREDENTIALS_FILE — checking GSC..."
256
+ # GSC: last 7 days via mcp__gsc__search_analytics (if MCP available)
257
+ # If MCP unavailable, log and skip — do not block
258
+ ANALYTICS_AVAILABLE=true
259
+ else
260
+ echo "Analytics pulse: SKIPPED — paths.credentials.file not set in forge.yaml or file absent"
261
+ ANALYTICS_AVAILABLE=false
262
+ fi
203
263
 
204
- For each finding from RECON_DATA, assign:
264
+ # Stripe balance: only if billing.enabled is true AND credentials are present
265
+ if [ "$BILLING_ENABLED" = "true" ] && [ "$ANALYTICS_AVAILABLE" = "true" ]; then
266
+ echo "Billing analytics: checking Stripe balance..."
267
+ # mcp__stripe__retrieve_balance — skip gracefully if MCP unavailable
268
+ else
269
+ echo "Billing analytics: SKIPPED — billing.enabled is false (or credentials absent)"
270
+ fi
271
+ ```
205
272
 
206
- | Signal | Priority | Type |
207
- |--------|----------|------|
208
- | Open ops-inbox item | P1 | bug |
209
- | Container unhealthy | P0 | bug |
210
- | Error spike (>5x normal) | P1 | bug |
211
- | Tier degradation (>20% drop) | P2 | bug |
212
- | CI consistently failing | P2 | bug |
213
- | Stale issue (>14 days) | — | (comment, don't create new issue) |
214
- | Orphaned workflow state | — | (fix via /cleanup, not new issue) |
215
- | Analytics decline (>20% week-over-week) | P2 | improvement |
216
- | New anti-bot pattern detected | P2 | feature |
273
+ ### 1E: Create issues from recon findings
217
274
 
218
- ### 2C: Create issues
275
+ For each finding (recurring CI failures, critical stale issue patterns):
219
276
 
220
- For each finding that passes dedup check, create a GitHub issue:
277
+ **Deduplication check first** — search existing open issues before creating:
278
+ ```bash
279
+ EXISTING_ISSUES=$(gh issue list $GH_FLAG --state open --limit 200 --json number,title \
280
+ --jq '.[] | "\(.number) \(.title)"' 2>/dev/null)
281
+ ```
221
282
 
283
+ For new findings that have no existing open duplicate:
222
284
  ```bash
223
- gh issue create \
224
- --title "{fix|feat|refactor}: {concise description}" \
225
- --label "{priority},{type}" \
226
- --body "$(cat <<'ISSUE_EOF'
285
+ if [ "$DRY_RUN" = "false" ]; then
286
+ gh issue create $GH_FLAG \
287
+ --title "fix: {finding_description}" \
288
+ --label "P2,bug" \
289
+ --body "$(cat <<'ISSUE_EOF'
227
290
  ## Problem
228
291
 
229
- {Description of the finding with specific data points. What's wrong or what's missing.}
292
+ {Description of the finding with specific data points.}
230
293
 
231
294
  ## Root Cause (if known)
232
295
 
233
- {What's causing the issue — specific code path, config gap, or behavior. If unknown: "Root cause unknown — investigation needed."}
296
+ {Specific root cause or "Root cause unknown — investigation needed."}
234
297
 
235
298
  ## Affected Files
236
299
 
237
300
  Files that need changes:
238
301
  1. `{filepath}` — {what needs to change}
239
- 2. `{filepath}` — {what needs to change}
240
302
 
241
303
  ## Acceptance Criteria
242
304
 
243
305
  - [ ] {Specific, testable criterion}
244
- - [ ] No regression in {related feature}
245
306
 
246
307
  ## Context
247
308
 
248
- Found by \`/autopilot\` cycle on {DATE}.
309
+ Found by \`/autopilot\` cycle on $(date -u +%Y-%m-%dT%H:%M:%SZ).
249
310
 
250
311
  ## Evidence
251
312
 
252
- {Logs, metrics, error messages — concrete data, not speculation}
253
-
254
- ## Suggested Approach
255
-
256
- {Brief suggestion — the /work-on investigation will validate this}
257
-
258
- ---
259
- *Created by autopilot recon cycle. Will be validated before any fix is applied.*
313
+ {Concrete data — log lines, failure counts, metrics}
260
314
  ISSUE_EOF
261
315
  )"
316
+ else
317
+ echo "[DRY-RUN] Would create issue: fix: {finding_description}"
318
+ fi
262
319
  ```
263
320
 
264
- **DRY_RUN check**: If `DRY_RUN == true`, print what would be created but don't actually create issues.
265
-
266
- ### 2D: Quick hygiene (inline, no separate /cleanup invocation)
267
-
268
- While we have the backlog data, fix obvious hygiene issues directly:
321
+ Store created issue numbers in `RECON_ISSUES` array for spec-edit impact analysis.
269
322
 
270
- ```bash
271
- # Close orphaned issues (merged PR but still open)
272
- for orphan in {ORPHAN_LIST}; do
273
- gh issue close $orphan --comment "Auto-closed by autopilot: linked PR was already merged."
274
- done
323
+ If `RECON_ONLY=true`, print recon report and **stop here**:
275
324
 
276
- # Fix stale labels on closed issues
277
- for stale in {STALE_LABEL_LIST}; do
278
- gh issue edit $stale --add-label "workflow:merged" --remove-label "workflow:in-review,workflow:building" 2>/dev/null || true
279
- done
280
325
  ```
326
+ ## Autopilot Recon Report — $(date -u +%Y-%m-%dT%H:%M:%SZ)
281
327
 
282
- Store list of created issues as `CREATED_ISSUES`.
283
-
284
- ---
285
-
286
- ## Phase 3: Cycle Report
287
-
288
- **Goal**: Present findings and actions taken to the user.
289
-
290
- Print a structured report:
328
+ ### Pipeline State
329
+ - staging→main PR: ${STAGING_PR_NUMBER:-none detected}
330
+ - In-flight issues (orphans recovered): $INFLIGHT_ISSUES
331
+ - Open fast-lane issues: $OPEN_FAST_LANE
291
332
 
292
- ```markdown
293
- ## Autopilot Cycle Report — {DATE}
294
-
295
- ### Production Health
296
- - Containers: {all healthy / N unhealthy}
297
- - Open ops-inbox items: {count}
298
- - Error patterns: {summary or "none"}
299
-
300
- ### Scraping Performance
301
- - Tier pass rates: T1 {X}%, T2 {X}%, T3 {X}%, T4 {X}%
302
- - Top failing domains: {list or "all healthy"}
303
- - New patterns: {any or "none detected"}
304
-
305
- ### CI/CD
306
- - Failure rate (24h): {X}/{Y} runs
307
- - Recurring failures: {list or "none"}
333
+ ### CI/CD (24h)
334
+ - Failure rate: $RECENT_FAILURES/$RECENT_RUNS
335
+ - Recurring: ${RECURRING:-none}
308
336
 
309
337
  ### Issue Backlog
310
- - Open: {P0: N, P1: N, P2: N, P3: N}
311
- - Stale (>14d): {count}
312
- - Orphaned: {count fixed}
313
-
314
- ### Analytics Pulse
315
- - Clicks (7d): {N} ({trend})
316
- - Revenue: ${N} (or "N/A — billing.enabled: false" if Stripe step was skipped)
338
+ - Open: P0=$P0_COUNT P1=$P1_COUNT P2=$P2_COUNT
339
+ - Stale (>14d, no workflow): $STALE_COUNT
317
340
 
318
341
  ### Actions Taken
319
- - Issues created: {count} ({list with numbers})
320
- - Issues commented: {count}
321
- - Orphans closed: {count}
322
- - Stale labels fixed: {count}
323
- - Spec-edit impact analyses logged: {count} (FORGE:AUTOPILOT_IMPACT — blast radius surfaced before spec fixes)
324
-
325
- ### Recommended Next Steps
326
- {Prioritized list of what the user should look at or deploy}
327
- ```
342
+ - Orphans recovered: $INFLIGHT_ISSUES
343
+ - Issues created from recon: ${#RECON_ISSUES[@]}
328
344
 
329
- If `DO_FIX == false`, **stop here**. Print:
330
- ```
331
- Run `/autopilot --fix` to also pick up and fix the top {FIX_LIMIT} issues.
345
+ Run without --recon-only to execute the full autonomous loop.
332
346
  ```
333
347
 
334
348
  ---
335
349
 
336
- ## Phase 4: Fix (Optional, requires --fix flag)
337
-
338
- **Goal**: Pick the highest-impact fixable issues and run them through `/work-on`.
339
-
340
- ### 4A: Select fix targets
350
+ ## Phase 2: Fast Lane Loop
341
351
 
342
- If `FIX_TARGETS` was set by Phase 0 (P0 issues), use those. Otherwise:
352
+ **Goal**: Work through all open unmilestoned issues until zero remain. Loop until the count is 0 or a safety cap is hit.
343
353
 
344
- Pick the top `FIX_LIMIT` issues by this priority:
345
- 1. P0 issues (always first)
346
- 2. P1 bugs (production-impacting)
347
- 3. Issues created by THIS cycle (freshest signal)
348
- 4. P2 bugs with `validated` or `needs-validation` label
349
- 5. Oldest P1 issues (prevent aging)
350
-
351
- **Exclude** from fix targets:
352
- - Issues with `needs-human` label
353
- - Issues in milestones (feature lane — don't autopilot feature work)
354
- - Issues already in `workflow:building` or `workflow:in-review`
355
- - Issues with open dependencies
354
+ **Overrides "never merge to main"**: This phase deploys staging→main after each iteration. `/autopilot` is the authorized deploy system.
356
355
 
357
356
  ```bash
358
- # Get candidates
359
- gh issue list --state open --label "bug" --limit 50 --json number,title,labels,createdAt \
360
- --jq 'sort_by(.labels | map(select(.name | startswith("priority:P"))) | .[0].name) | .[:{FIX_LIMIT}] | .[] | "#\(.number) \(.title)"'
361
- ```
362
-
363
- ### 4B: Present fix plan to user
357
+ FAST_LANE_ITERATIONS=0
358
+ MAX_FAST_LANE_ITERATIONS=20 # safety cap — prevents infinite loop on stuck state
359
+ FAST_LANE_DEPLOYS=0
360
+ FAST_LANE_FINDINGS_BOUNCED=0
364
361
 
365
- **MANDATORY CHECKPOINT — do NOT proceed without user confirmation.**
366
-
367
- **Exception**: when `AUTO_YES = true`, classify each fix target by risk tier before prompting. Low-risk fixes are auto-approved; high-risk fixes still require confirmation.
368
-
369
- **Risk classification** (evaluate per-issue at this step):
370
-
371
- ```
372
- LOW_RISK = all of the following are true:
373
- - Priority is P2 or P3 (not P0 or P1)
374
- - Affects only a single file (from issue body or recon finding)
375
- - No sensitive-domain labels: auth, billing, database, security, payments, gdpr
376
-
377
- HIGH_RISK = any of the following:
378
- - Priority P0 or P1
379
- - Multi-file change (2+ files)
380
- - Has a sensitive-domain label (auth, billing, database, security, payments, gdpr)
381
- - Issue has `needs-human` label
382
- ```
362
+ echo "=== Fast Lane Loop ==="
383
363
 
384
- When `AUTO_YES = true`:
385
- - **LOW_RISK issues**: auto-approve, log `[AUTO-APPROVED: low-risk]`, proceed to 4B.5/4C without waiting.
386
- - **HIGH_RISK issues**: present the checkpoint below and wait for user response. If user skips, remove from fix targets and continue with remaining low-risk issues.
364
+ while true; do
365
+ FAST_LANE_ITERATIONS=$((FAST_LANE_ITERATIONS + 1))
387
366
 
388
- When `AUTO_YES = false` (default): present the full checkpoint for ALL fix targets and wait.
367
+ if [ "$FAST_LANE_ITERATIONS" -gt "$MAX_FAST_LANE_ITERATIONS" ]; then
368
+ echo "Fast lane loop: reached max iterations ($MAX_FAST_LANE_ITERATIONS) — breaking to prevent infinite loop"
369
+ break
370
+ fi
389
371
 
390
- ```markdown
391
- ## Autopilot Fix Plan
372
+ # Re-query open unmilestoned issues — always re-read, never trust a cached count
373
+ OPEN_UNMILESTONED=$(gh issue list $GH_FLAG \
374
+ --state open \
375
+ --limit 200 \
376
+ --json number,title,milestone,labels \
377
+ --jq '[.[] | select(
378
+ .milestone == null and
379
+ (.labels | map(.name) | any(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:decomposed") | not)
380
+ )] | length' \
381
+ 2>/dev/null || echo '0')
382
+
383
+ echo "Fast lane iteration $FAST_LANE_ITERATIONS: $OPEN_UNMILESTONED open unmilestoned issue(s)"
384
+
385
+ if [ "$OPEN_UNMILESTONED" -eq 0 ]; then
386
+ echo "Fast lane: zero open unmilestoned issues — loop complete"
387
+ break
388
+ fi
392
389
 
393
- I'll run `/work-on` for these {N} issues:
390
+ # Step 1: Orchestrate all open fast-lane issues in parallel via /orchestrate
391
+ echo "=== Fast Lane Iteration $FAST_LANE_ITERATIONS: Orchestrating $OPEN_UNMILESTONED issues ==="
392
+ if [ "$DRY_RUN" = "false" ]; then
393
+ Skill("orchestrate", args="fast-lane")
394
+ else
395
+ echo "[DRY-RUN] Would invoke: Skill(orchestrate, fast-lane)"
396
+ fi
394
397
 
395
- | # | Title | Priority | Estimated Scope | Risk Tier |
396
- |---|-------|----------|-----------------|-----------|
397
- | {number} | {title} | {priority} | {small/medium/large} | LOW / HIGH |
398
+ # Step 2: Deploy staging→main if staging has new commits
399
+ git fetch origin "$STAGING_BRANCH" "$DEFAULT_BRANCH" 2>/dev/null || true
400
+ STAGING_AHEAD_NOW=$(git rev-list --count "origin/${DEFAULT_BRANCH}..origin/${STAGING_BRANCH}" 2>/dev/null || echo '0')
401
+
402
+ if [ "$STAGING_AHEAD_NOW" -gt 0 ]; then
403
+ echo "staging is $STAGING_AHEAD_NOW commit(s) ahead — deploying via /deploy-pr..."
404
+ if [ "$DRY_RUN" = "false" ]; then
405
+ DEPLOY_RESULT=$(Skill("deploy-pr", args="staging"))
406
+ # Parse structured JSON result from deploy-pr
407
+ DEPLOY_STATUS=$(echo "$DEPLOY_RESULT" | jq -r '.status // empty' 2>/dev/null || echo '')
408
+ [ -z "$DEPLOY_STATUS" ] && DEPLOY_STATUS=$(echo "$DEPLOY_RESULT" | grep -oE '"status":"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"' || echo 'unknown')
409
+ echo "Deploy status: $DEPLOY_STATUS"
410
+ [ "$DEPLOY_STATUS" = "merged" ] && FAST_LANE_DEPLOYS=$((FAST_LANE_DEPLOYS + 1))
411
+ else
412
+ echo "[DRY-RUN] Would invoke: Skill(deploy-pr, staging)"
413
+ fi
414
+ else
415
+ echo "staging is not ahead of $DEFAULT_BRANCH — no deploy needed this iteration"
416
+ fi
398
417
 
399
- Each issue goes through: investigate → validate → build → review → merge to staging.
400
- Nothing merges to `main` — you deploy when ready.
418
+ # Step 3: Recover any newly orphaned issues before next iteration
419
+ if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ] && [ "$DRY_RUN" = "false" ]; then
420
+ Skill("recover-orphans", args="--since 2")
421
+ elif [ "$DRY_RUN" = "true" ]; then
422
+ echo "[DRY-RUN] Would invoke: Skill(recover-orphans, --since 2)"
423
+ fi
401
424
 
402
- {If AUTO_YES and any HIGH_RISK: "Note: {N_low} low-risk issues were auto-approved. The {N_high} high-risk issue(s) above require your confirmation."}
425
+ # Count review findings that bounced to fast lane (unmilestoned review-finding issues)
426
+ NEW_FINDINGS=$(gh issue list $GH_FLAG \
427
+ --state open \
428
+ --limit 200 \
429
+ --json number,milestone,labels \
430
+ --jq '[.[] | select(.milestone == null and (.labels | map(.name) | any(. == "review-finding")))] | length' \
431
+ 2>/dev/null || echo '0')
432
+ [ "$NEW_FINDINGS" -gt 0 ] && FAST_LANE_FINDINGS_BOUNCED=$((FAST_LANE_FINDINGS_BOUNCED + NEW_FINDINGS))
433
+ echo "End of iteration $FAST_LANE_ITERATIONS — review findings in fast lane: $NEW_FINDINGS"
434
+ done
403
435
 
404
- **Proceed?** (yes / adjust / skip)
436
+ echo "Fast lane complete: $FAST_LANE_ITERATIONS iteration(s), $FAST_LANE_DEPLOYS deploy(s)"
405
437
  ```
406
438
 
407
- Wait for user response on high-risk issues. If they adjust, re-select. If they skip all, end the cycle.
408
-
409
- ### 4B.5: Spec-Edit Impact Analysis (MANDATORY before any spec-touching fix) <!-- Added: forge#870 -->
410
-
411
- **Goal**: Before autopilot dispatches a fix that edits ForgeDock's own command specs (`commands/*.md`), surface the blast radius of the change so the edit doesn't silently break downstream consumers (every spec that reads a changed FORGE annotation, every command in a changed sub-phase chain, every script/label a touched node feeds).
439
+ ---
412
440
 
413
- This step **consumes the read-only query interface** shipped by the Spec Knowledge Graph (`scripts/graph-query.sh`, subcommand `impact`). It does **not** reimplement graph traversal — the blast radius is computed once, in `graph-query.sh`, and autopilot only reads its output.
441
+ ## Phase 3: Milestone Loop
414
442
 
415
- **When it runs**: for each approved fix target whose affected files (from the issue body's `## Affected Files` section, or the recon finding that created it) include any path matching `commands/*.md`. Non-spec fixes skip this step entirely.
443
+ **Goal**: Ship each milestone with open issues, in order of completion percentage (highest first). Skip milestones at 0%.
416
444
 
417
- **Graceful degradation**: `graph-query.sh` ships only with the Spec Knowledge Graph. If it is absent (fast-lane repos, or a checkout predating the milestone), **skip with a noted reason — never block the cycle**.
445
+ **Each milestone cycle**: orchestrate all open issues in the milestone → ship milestone branch to staging → deploy staging to main.
418
446
 
419
447
  ```bash
420
- GRAPH_QUERY="$(git rev-parse --show-toplevel)/scripts/graph-query.sh"
421
-
422
- # For each approved fix target that edits commands/*.md:
423
- for issue in $SPEC_FIX_TARGETS; do
424
- if [ ! -x "$GRAPH_QUERY" ]; then
425
- echo "skip impact analysis for #$issue — graph-query.sh not present (Spec Knowledge Graph not installed)"
448
+ MILESTONE_ITERATIONS=0
449
+ MILESTONE_DEPLOYS=0
450
+
451
+ echo "=== Milestone Loop ==="
452
+
453
+ # Get all open milestones with completion percentages
454
+ MILESTONES=$(gh api "repos/$GH_REPO/milestones" \
455
+ --jq '[.[] | {
456
+ number: .number,
457
+ title: .title,
458
+ slug: (.title | ascii_downcase | gsub("[^a-z0-9]+"; "-") | ltrimstr("-") | rtrimstr("-")),
459
+ open_issues: .open_issues,
460
+ closed_issues: .closed_issues,
461
+ completion_pct: (if (.open_issues + .closed_issues) > 0 then ((.closed_issues * 100) / (.open_issues + .closed_issues) | floor) else 0 end)
462
+ }] | sort_by(-.completion_pct)' \
463
+ 2>/dev/null || echo '[]')
464
+
465
+ MILESTONE_COUNT=$(echo "$MILESTONES" | jq 'length' 2>/dev/null || echo '0')
466
+ echo "Found $MILESTONE_COUNT milestone(s)"
467
+
468
+ echo "$MILESTONES" | jq -r '.[] | "\(.completion_pct)% — \(.title) (\(.open_issues) open, \(.closed_issues) closed)"'
469
+
470
+ # Process each milestone — sorted by completion% descending, skip 0%
471
+ echo "$MILESTONES" | jq -c '.[]' | while IFS= read -r milestone; do
472
+ MS_TITLE=$(echo "$milestone" | jq -r '.title')
473
+ MS_SLUG=$(echo "$milestone" | jq -r '.slug')
474
+ MS_OPEN=$(echo "$milestone" | jq -r '.open_issues')
475
+ MS_PCT=$(echo "$milestone" | jq -r '.completion_pct')
476
+
477
+ if [ "$MS_PCT" -eq 0 ]; then
478
+ echo "Skipping milestone '$MS_TITLE' (0% complete — no issues closed yet)"
426
479
  continue
427
480
  fi
428
481
 
429
- # Derive the spec node(s) the fix changes. Prefer the FORGE annotation or
430
- # command/sub-phase named in the issue's Affected Files / recon finding.
431
- # graph-query.sh normalizes all of these forms:
432
- # FORGE:CONTRACT == CONTRACT == ann:FORGE:CONTRACT
433
- # work-on:build:implement == cmd:work-on:build:implement
434
- # classify-lane.sh == script:classify-lane.sh
435
- # workflow:merged == label:workflow:merged
436
- for node in $CHANGED_SPEC_NODES; do
437
- echo "### Impact of changing $node (fix #$issue)"
438
- bash "$GRAPH_QUERY" impact "$node" --human
439
- done
440
- done
441
- ```
442
-
443
- **Log the result in a FORGE annotation.** Post one `FORGE:AUTOPILOT_IMPACT` comment per spec-touching fix target on its issue, BEFORE invoking `/work-on` in 4C:
444
-
445
- ```bash
446
- gh issue comment $issue --body "<!-- FORGE:AUTOPILOT_IMPACT -->
447
- ## Spec-Edit Impact Analysis
448
-
449
- **Changed spec node(s)**: \`$CHANGED_SPEC_NODES\`
450
- **Query**: \`graph-query.sh impact <node> --human\` (Spec Knowledge Graph, read-only)
482
+ echo "=== Processing milestone: '$MS_TITLE' ($MS_PCT% complete, $MS_OPEN open) ==="
451
483
 
452
- ### Blast Radius (downstream consumers of this change)
453
- \`\`\`
454
- $IMPACT_OUTPUT
455
- \`\`\`
456
-
457
- Every command/spec listed above reads the changed annotation or sits in the changed sub-phase chain. The \`/work-on\` build for this fix MUST keep these consumers working — verify the changed field/marker is still produced in the shape they expect.
484
+ # Step 1: Orchestrate open issues in this milestone
485
+ if [ "$MS_OPEN" -gt 0 ]; then
486
+ if [ "$DRY_RUN" = "false" ]; then
487
+ Skill("orchestrate", args="milestone $MS_SLUG")
488
+ else
489
+ echo "[DRY-RUN] Would invoke: Skill(orchestrate, milestone $MS_SLUG)"
490
+ fi
491
+ else
492
+ echo "Milestone '$MS_TITLE' has no open issues — checking if branch needs deploy"
493
+ fi
458
494
 
459
- <!-- FORGE:AUTOPILOT_IMPACT:COMPLETE -->"
460
- ```
495
+ # Step 2: Ship milestone branch → staging via /deploy-pr
496
+ MILESTONE_BRANCH="milestone/$MS_SLUG"
497
+ MILESTONE_BRANCH_EXISTS=$(git ls-remote --exit-code origin "$MILESTONE_BRANCH" >/dev/null 2>&1 && echo "true" || echo "false")
498
+
499
+ if [ "$MILESTONE_BRANCH_EXISTS" = "true" ]; then
500
+ echo "Shipping $MILESTONE_BRANCH → staging..."
501
+ if [ "$DRY_RUN" = "false" ]; then
502
+ MS_RESULT=$(Skill("deploy-pr", args="$MILESTONE_BRANCH"))
503
+ MS_STATUS=$(echo "$MS_RESULT" | jq -r '.status // empty' 2>/dev/null || echo '')
504
+ [ -z "$MS_STATUS" ] && MS_STATUS=$(echo "$MS_RESULT" | grep -oE '"status":"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"' || echo 'unknown')
505
+ echo "Milestone ship status: $MS_STATUS"
506
+
507
+ if [ "$MS_STATUS" = "merged" ]; then
508
+ MILESTONE_DEPLOYS=$((MILESTONE_DEPLOYS + 1))
509
+
510
+ # Step 3: After milestone merges to staging, deploy staging → main
511
+ git fetch origin "$STAGING_BRANCH" "$DEFAULT_BRANCH" 2>/dev/null || true
512
+ STAGING_AHEAD_MS=$(git rev-list --count "origin/${DEFAULT_BRANCH}..origin/${STAGING_BRANCH}" 2>/dev/null || echo '0')
513
+ if [ "$STAGING_AHEAD_MS" -gt 0 ]; then
514
+ echo "Milestone merged to staging — deploying staging → $DEFAULT_BRANCH..."
515
+ MAIN_RESULT=$(Skill("deploy-pr", args="staging"))
516
+ MAIN_STATUS=$(echo "$MAIN_RESULT" | jq -r '.status // empty' 2>/dev/null || echo 'unknown')
517
+ echo "Main deploy status: $MAIN_STATUS"
518
+ [ "$MAIN_STATUS" = "merged" ] && FAST_LANE_DEPLOYS=$((FAST_LANE_DEPLOYS + 1))
519
+ fi
520
+ fi
521
+ else
522
+ echo "[DRY-RUN] Would invoke: Skill(deploy-pr, $MILESTONE_BRANCH)"
523
+ echo "[DRY-RUN] If merged, would invoke: Skill(deploy-pr, staging)"
524
+ fi
525
+ else
526
+ echo "Milestone branch '$MILESTONE_BRANCH' not found on remote — skipping deploy"
527
+ fi
461
528
 
462
- If `graph-query.sh` was absent, post the same annotation with the blast-radius block set to `(skipped — Spec Knowledge Graph not installed)` so the skip is auditable.
529
+ MILESTONE_ITERATIONS=$((MILESTONE_ITERATIONS + 1))
463
530
 
464
- **Worked example** — autopilot picks up a fix that renames a field inside the `FORGE:CONTRACT` annotation. Before dispatching `/work-on`, it runs:
531
+ # Step 4: Review findings from milestones are unmilestoned — they loop back to fast lane automatically
532
+ MS_FINDINGS=$(gh issue list $GH_FLAG \
533
+ --state open \
534
+ --limit 50 \
535
+ --json number,milestone,labels \
536
+ --jq '[.[] | select(.milestone == null and (.labels | map(.name) | any(. == "review-finding")))] | length' \
537
+ 2>/dev/null || echo '0')
538
+ [ "$MS_FINDINGS" -gt 0 ] && echo "NOTE: $MS_FINDINGS review finding(s) now in fast lane — processed next cycle"
539
+ done
465
540
 
466
- ```bash
467
- $ graph-query.sh impact FORGE:CONTRACT --human
468
- Impact (blast radius) of changing ann:FORGE:CONTRACT:
469
- cmd:milestone
470
- cmd:orchestrate
471
- cmd:resume
472
- cmd:review-pr
473
- cmd:work-on
474
- cmd:work-on-monolithic
475
- cmd:work-on:build
476
- cmd:work-on:build:implement
477
- cmd:work-on:review
541
+ echo "Milestone loop complete: $MILESTONE_ITERATIONS milestone(s) processed, $MILESTONE_DEPLOYS milestone ship(s)"
478
542
  ```
479
543
 
480
- The summary makes the nine downstream consumers explicit before the edit lands, so the contract-field rename is reviewed against every reader — not discovered as a broken consumer later.
544
+ ---
481
545
 
482
- **`FORGE:AUTOPILOT_IMPACT`** is a leaf annotation: autopilot is its only writer and no downstream pipeline phase consumes it. It exists to record the blast radius on the issue thread for human and reviewer visibility.
546
+ ## Phase 4: Final Report
483
547
 
484
- ### 4C: Execute fixes
548
+ Print a summary of everything done in this autopilot run:
485
549
 
486
- For each approved fix target, invoke `/work-on` via the Skill tool:
550
+ ```bash
551
+ CYCLE_END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
552
+
553
+ # Re-check final open issue count
554
+ FINAL_OPEN=$(gh issue list $GH_FLAG --state open --limit 200 --json number --jq 'length' 2>/dev/null || echo '0')
555
+ FINAL_FAST_LANE=$(gh issue list $GH_FLAG --state open --limit 200 --json number,milestone \
556
+ --jq '[.[] | select(.milestone == null)] | length' 2>/dev/null || echo '0')
557
+
558
+ echo ""
559
+ echo "╔═══════════════════════════════════════════════════╗"
560
+ echo "║ Autopilot Complete ║"
561
+ echo "╠═══════════════════════════════════════════════════╣"
562
+ echo "║ ║"
563
+
564
+ cat <<REPORT
565
+ ## Autopilot Cycle Report — $CYCLE_END
566
+
567
+ ### Pipeline State at Start
568
+ - staging→main PR: ${STAGING_PR_NUMBER:-none}
569
+ - In-flight issues recovered: $INFLIGHT_ISSUES
570
+ - Open fast-lane issues at start: $OPEN_FAST_LANE
571
+ - Milestones found: ${MILESTONE_COUNT:-0}
572
+
573
+ ### Recon
574
+ - CI failures (24h): $RECENT_FAILURES/$RECENT_RUNS
575
+ - Recurring failures: ${RECURRING:-none}
576
+ - Issues created from recon: ${#RECON_ISSUES[@]:-0}
577
+
578
+ ### Fast Lane
579
+ - Iterations: $FAST_LANE_ITERATIONS
580
+ - Deploys (staging→$DEFAULT_BRANCH): $FAST_LANE_DEPLOYS
581
+ - Review findings bounced to fast lane: $FAST_LANE_FINDINGS_BOUNCED
582
+
583
+ ### Milestone Loop
584
+ - Milestones processed: $MILESTONE_ITERATIONS
585
+ - Milestone ships: $MILESTONE_DEPLOYS
586
+
587
+ ### Final State
588
+ - Open issues: $FINAL_OPEN total ($FINAL_FAST_LANE unmilestoned)
589
+ REPORT
590
+
591
+ if [ "$FINAL_FAST_LANE" -eq 0 ]; then
592
+ echo ""
593
+ echo "Zero open unmilestoned issues remain. Pipeline is clean."
594
+ else
595
+ echo ""
596
+ echo "$FINAL_FAST_LANE unmilestoned issue(s) remain. These may require human review (needs-human label) or have open dependencies:"
597
+ gh issue list $GH_FLAG --state open --limit 20 --json number,title,labels,milestone \
598
+ --jq '.[] | select(.milestone == null) | " #\(.number) \(.title) [\(.labels | map(.name) | join(","))]"' \
599
+ 2>/dev/null || true
600
+ fi
487
601
 
488
- ```
489
- Skill(skill: "work-on", args: "{ISSUE_NUMBER}")
602
+ echo ""
603
+ echo "╚═══════════════════════════════════════════════════╝"
490
604
  ```
491
605
 
492
- Run them **sequentially** (not parallel) — each `/work-on` invocation is heavyweight and benefits from full context. If one fails, continue to the next.
606
+ ---
493
607
 
494
- After each `/work-on` completes, record the result:
495
- ```
496
- FIX_RESULTS.push({
497
- issue: NUMBER,
498
- outcome: "merged" | "invalid" | "failed" | "needs-human",
499
- pr: PR_NUMBER or null,
500
- branch: BRANCH_NAME
501
- })
502
- ```
608
+ ## Spec-Edit Impact Analysis (MANDATORY before any spec-touching recon fix)
503
609
 
504
- ### 4D: Fix summary
610
+ <!-- Added: forge#870 -->
505
611
 
506
- ```markdown
507
- ## Fix Results
612
+ Before autopilot dispatches a recon-created fix (via /orchestrate → /work-on) that edits ForgeDock's own command specs (`commands/*.md`), surface the blast radius via `graph-query.sh`. This prevents spec edits from silently breaking downstream consumers.
508
613
 
509
- | Issue | Outcome | PR |
510
- |-------|---------|-----|
511
- | #{N} | {outcome} | #{PR} |
614
+ **When it runs**: for each issue in `RECON_ISSUES` whose affected files include any path matching `commands/*.md`.
512
615
 
513
- **Merged to staging**: {count}
514
- **Needs attention**: {list}
515
- **Spec-edit impact analyses**: {count} blast-radius summaries logged (FORGE:AUTOPILOT_IMPACT) before spec fixes
616
+ ```bash
617
+ GRAPH_QUERY="$(git rev-parse --show-toplevel)/scripts/graph-query.sh"
618
+ RECON_ISSUES=${RECON_ISSUES:-()}
516
619
 
517
- When ready to deploy, merge `staging` → `main` via GitHub.
518
- ```
620
+ for issue in "${RECON_ISSUES[@]}"; do
621
+ AFFECTED_SPECS=$(gh issue view "$issue" $GH_FLAG --json body --jq '.body' \
622
+ | grep -oE '`commands/[^`]+\.md`' | tr -d '`' | head -5)
623
+ if [ -z "$AFFECTED_SPECS" ]; then continue; fi
519
624
 
520
- ### 4E: Deploy Train Check <!-- Added: forge#1332 -->
625
+ IMPACT_OUTPUT=""
626
+ if [ ! -x "$GRAPH_QUERY" ]; then
627
+ IMPACT_OUTPUT="(skipped — Spec Knowledge Graph not installed)"
628
+ echo "skip impact analysis for #$issue — graph-query.sh not present"
629
+ else
630
+ for spec_file in $AFFECTED_SPECS; do
631
+ NODE=$(basename "$spec_file" .md)
632
+ NODE_IMPACT=$(bash "$GRAPH_QUERY" impact "$NODE" --human 2>/dev/null || echo "(no edges for $NODE)")
633
+ IMPACT_OUTPUT="${IMPACT_OUTPUT}### Impact of $NODE:
634
+ ${NODE_IMPACT}
635
+
636
+ "
637
+ done
638
+ fi
521
639
 
522
- After fixes are merged to staging, check the deploy train state. The deploy train is the single rolling staging→main PR — one per deploy cycle. Do NOT open a new staging→main PR if one is already open.
640
+ if [ "$DRY_RUN" = "false" ]; then
641
+ gh issue comment "$issue" $GH_FLAG --body "<!-- FORGE:AUTOPILOT_IMPACT -->
642
+ ## Spec-Edit Impact Analysis
523
643
 
524
- ```bash
525
- # Check for an open staging→main deploy PR (the current train)
526
- TRAIN_PR=$(gh pr list {GH_FLAG} \
527
- --head "{STAGING_BRANCH}" \
528
- --base "{DEFAULT_BRANCH}" \
529
- --state open \
530
- --json number,url \
531
- --jq '.[0] // empty' 2>/dev/null)
644
+ **Changed spec node(s)**: \`$AFFECTED_SPECS\`
645
+ **Query**: \`graph-query.sh impact <node> --human\` (Spec Knowledge Graph, read-only)
532
646
 
533
- TRAIN_PR_NUMBER=$(echo "$TRAIN_PR" | jq -r '.number // empty')
647
+ ### Blast Radius (downstream consumers of this change)
648
+ \`\`\`
649
+ $IMPACT_OUTPUT
650
+ \`\`\`
534
651
 
535
- if [ -n "$TRAIN_PR_NUMBER" ]; then
536
- # Train exists — check if it is held by open findings
537
- HELD_BY_COUNT=$(gh issue list {GH_FLAG} \
538
- --label "review-finding" \
539
- --state open \
540
- --search "PR #${TRAIN_PR_NUMBER}" \
541
- --json number --jq '. | length' 2>/dev/null || echo 0)
652
+ Every command/spec listed above reads the changed annotation or sits in the changed sub-phase chain. The \`/work-on\` build for this fix MUST keep these consumers working.
542
653
 
543
- if [ "$HELD_BY_COUNT" -gt 0 ]; then
544
- echo "Deploy train PR #${TRAIN_PR_NUMBER} is HELD by ${HELD_BY_COUNT} open finding(s)."
545
- echo "The fixes merged in this autopilot cycle may resolve some of them."
546
- echo "Re-run /review-pr-staging ${TRAIN_PR_NUMBER} to re-evaluate the train gate."
654
+ <!-- FORGE:AUTOPILOT_IMPACT:COMPLETE -->"
547
655
  else
548
- echo "Deploy train PR #${TRAIN_PR_NUMBER} is CLEAR — no open findings. Ready to merge when you authorize."
656
+ echo "[DRY-RUN] Would post FORGE:AUTOPILOT_IMPACT on issue #$issue"
549
657
  fi
550
- else
551
- echo "No open staging→main PR found. The merged fixes are queued in staging."
552
- echo "Run /review-pr-staging to open a new train PR when ready."
553
- fi
658
+ done
554
659
  ```
555
660
 
556
- **Deploy policy**: Autopilot NEVER merges staging→main. It merges issue fixes TO staging only. The train PR is a human-gated merge — the user decides when to ship.
661
+ **`FORGE:AUTOPILOT_IMPACT`** is a leaf annotation: autopilot is its only writer and no downstream pipeline phase consumes it. It records blast radius on the issue thread for human and reviewer visibility.
557
662
 
558
663
  ---
559
664
 
560
665
  ## Safety Rules
561
666
 
562
- 1. **NEVER merge to `main`** — all fixes go to `staging`. User deploys manually.
563
- 2. **NEVER skip investigation** — every fix goes through full `/work-on` (investigate → validate → build → review → merge).
564
- 3. **NEVER fix `needs-human` issues** — these require human judgment (legal, external services, etc.).
565
- 4. **NEVER fix milestone issues** — feature work is scoped to milestones, not autopilot.
566
- 5. **NEVER create duplicate issues** — always dedup against existing open issues first.
567
- 6. **NEVER run more than FIX_LIMIT fixes per cycle** — prevent runaway resource consumption.
568
- 7. **If a fix fails, move on** — don't retry. Log it and let the next cycle or human handle it.
569
- 8. **DRY_RUN means NO side effects** — no issues created, no PRs, no label changes. Report only.
570
- 9. **P0 overrides everything** — if P0 exists, skip normal recon and fix P0 immediately.
571
- 10. **Always gate high-risk fixes on user approval** — Phase 4B checkpoint is non-negotiable for P0/P1, multi-file, or sensitive-domain changes. `--yes` only auto-approves LOW_RISK tier (P2/P3, single-file, non-sensitive).
572
-
573
- ---
574
-
575
- ## Recursion: How Cycles Compound
576
-
577
- Each cycle feeds the next:
578
- ```
579
- Cycle N: CI recon detects recurring lint failures across 3 files → autopilot triage creates issue #X
580
- Cycle N+1: autopilot picks up #X → /work-on adds pre-commit hook → merged to staging
581
- Cycle N+2: CI recon shows lint failures resolved — no new issue needed
582
- ```
583
-
584
- ```
585
- Cycle N: /analytics finds CTR dropped on /pricing → creates issue #Y
586
- Cycle N+1: autopilot picks up #Y → /work-on rewrites SERP snippet → merged
587
- Cycle N+2: /analytics shows CTR recovered → improvement confirmed
588
- ```
589
-
590
- The platform gets better every cycle. Issues that aren't picked up age and rise in priority. Issues that keep recurring indicate a deeper structural problem — autopilot will eventually create a "meta" issue about the pattern.
667
+ 1. **Overrides "never merge to main"** — `/autopilot` IS the authorized deploy system. staging→main is normal operation.
668
+ 2. **Never skip investigation** — all issues go through full `/work-on` pipeline (via /orchestrate) before any fix lands.
669
+ 3. **Never process `needs-human` issues** — /orchestrate and /work-on skip these automatically.
670
+ 4. **Never create duplicate issues** — always dedup against existing open issues before creating.
671
+ 5. **Loop safety cap** — max 20 fast-lane iterations prevents infinite loops on stuck state.
672
+ 6. **DRY_RUN means NO side effects** — no issues created, no PRs merged, no labels changed. Report only.
673
+ 7. **Graceful sub-skill degradation** — if /recover-orphans is not installed, log a warning and continue.
674
+ 8. **State always re-read** — never trust a cached issue count. Re-query GitHub at each loop iteration.
675
+ 9. **deploy-pr result is authoritative** — if status is not "merged", do not assume the deploy succeeded. Log and continue the loop.
591
676
 
592
677
  ---
593
678
 
594
679
  ## Operational Notes
595
680
 
596
- - **Runtime**: Recon-only ~3-5 min. Full cycle with fixes ~15-45 min depending on issue count.
597
- - **Token budget**: Recon is cheap (mostly MCP + API calls). Fixes are expensive (full /work-on per issue).
598
- - **Idempotent**: Safe to run multiple times — dedup prevents duplicate issues, /work-on resumes from checkpoints.
599
- - **Pairs with /loop**: Run `/loop 4h /autopilot` for continuous improvement, `/loop 4h /autopilot --fix --limit 2` for autonomous fixing with a human checkpoint every cycle, or `/loop 4h /autopilot --fix --yes` for fully unattended low-risk fixes (high-risk issues still surface for approval).
600
- - **Event-driven complement**: `/autopilot` is a periodic cycle. When a specific production signal fires (metric regression, incident, GEO gap), use `/signal-planner` instead — it converts the signal into a dependency-ordered issue DAG, executes via `/orchestrate`, and verifies the originating signal is resolved after the work merges. Use `/autopilot` for scheduled health sweeps; use `/signal-planner` for targeted signal-driven response.
681
+ - **Runtime**: Recon-only ~2-3 min. Full cycle time depends on open issue count — typically 20-90 min per loop.
682
+ - **Token budget**: Recon is cheap (mostly API calls). Each orchestrate+deploy cycle is expensive (full /work-on per issue via /orchestrate).
683
+ - **Idempotent**: Safe to run multiple times — /work-on resumes from pipeline checkpoints, /deploy-pr detects existing open PRs.
684
+ - **Pairs with /loop**: Run `/loop 4h /autopilot` for continuous improvement cycles.
685
+ - **Event-driven complement**: `/autopilot` is a periodic loop. For targeted signal-driven response (metric regression, incident, GEO gap), use `/signal-planner` instead — it converts a specific signal into a dependency-ordered issue DAG, executes via `/orchestrate`, and verifies the originating signal is resolved after the work merges. Use `/autopilot` for scheduled sweeps; use `/signal-planner` for targeted responses.