forgedock 1.1.2 → 1.1.3
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/bin/engine/phases.mjs +8 -6
- package/bin/runner.mjs +3 -2
- package/commands/autopilot.md +510 -425
- package/commands/deploy-pr.md +485 -0
- package/commands/fix-ci.md +577 -0
- package/commands/recover-orphans.md +576 -0
- package/docs/CI.md +14 -0
- package/docs/eval/README.md +7 -4
- package/docs/site/command-reference.md +49 -0
- package/package.json +1 -1
- package/packages/protocol/src/parse.js +27 -2
package/commands/autopilot.md
CHANGED
|
@@ -1,37 +1,33 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Autonomous
|
|
3
|
-
argument-hint: [--
|
|
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 —
|
|
9
|
+
# /autopilot — Autonomous Deploy Loop
|
|
8
10
|
|
|
9
|
-
**Input**: $ARGUMENTS (default:
|
|
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
|
|
14
|
-
- `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to
|
|
15
|
-
- `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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>`.
|
|
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) |
|
|
43
|
-
| `--
|
|
44
|
-
| `--
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
##
|
|
61
|
-
|
|
62
|
-
**Goal**: Understand what happened since the last cycle.
|
|
57
|
+
## Config Preamble (MANDATORY — run before any phase)
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
Read all `forge.yaml` config variables before any logic runs:
|
|
65
60
|
|
|
66
61
|
```bash
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
**Goal**: Read current GitHub state and determine where the pipeline is. Always start here — never assume clean state.
|
|
97
88
|
|
|
98
|
-
###
|
|
89
|
+
### 0A: Detect open staging→main PR
|
|
99
90
|
|
|
100
91
|
```bash
|
|
101
|
-
gh
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
### 0C: Detect in-flight issues (stuck in intermediate workflow states)
|
|
115
122
|
|
|
116
|
-
|
|
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
|
-
|
|
133
|
+
### 0D: Detect staging vs main delta
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
echo "CI (24h): $RECENT_FAILURES/$RECENT_RUNS failures"
|
|
189
218
|
|
|
190
|
-
|
|
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
|
-
|
|
226
|
+
### 1C: Issue Backlog Health
|
|
193
227
|
|
|
194
228
|
```bash
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
250
|
+
### 1D: Analytics Pulse (optional — forge.yaml-gated)
|
|
201
251
|
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
For each finding (recurring CI failures, critical stale issue patterns):
|
|
219
276
|
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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.
|
|
292
|
+
{Description of the finding with specific data points.}
|
|
230
293
|
|
|
231
294
|
## Root Cause (if known)
|
|
232
295
|
|
|
233
|
-
{
|
|
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
|
|
309
|
+
Found by \`/autopilot\` cycle on $(date -u +%Y-%m-%dT%H:%M:%SZ).
|
|
249
310
|
|
|
250
311
|
## Evidence
|
|
251
312
|
|
|
252
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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:
|
|
311
|
-
- Stale (>14d):
|
|
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
|
-
-
|
|
320
|
-
- Issues
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
+
echo "Fast lane complete: $FAST_LANE_ITERATIONS iteration(s), $FAST_LANE_DEPLOYS deploy(s)"
|
|
405
437
|
```
|
|
406
438
|
|
|
407
|
-
|
|
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
|
-
|
|
441
|
+
## Phase 3: Milestone Loop
|
|
414
442
|
|
|
415
|
-
**
|
|
443
|
+
**Goal**: Ship each milestone with open issues, in order of completion percentage (highest first). Skip milestones at 0%.
|
|
416
444
|
|
|
417
|
-
**
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
$
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
+
MILESTONE_ITERATIONS=$((MILESTONE_ITERATIONS + 1))
|
|
463
530
|
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
+
---
|
|
481
545
|
|
|
482
|
-
|
|
546
|
+
## Phase 4: Final Report
|
|
483
547
|
|
|
484
|
-
|
|
548
|
+
Print a summary of everything done in this autopilot run:
|
|
485
549
|
|
|
486
|
-
|
|
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
|
-
|
|
602
|
+
echo ""
|
|
603
|
+
echo "╚═══════════════════════════════════════════════════╝"
|
|
490
604
|
```
|
|
491
605
|
|
|
492
|
-
|
|
606
|
+
---
|
|
493
607
|
|
|
494
|
-
|
|
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
|
-
|
|
610
|
+
<!-- Added: forge#870 -->
|
|
505
611
|
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
616
|
+
```bash
|
|
617
|
+
GRAPH_QUERY="$(git rev-parse --show-toplevel)/scripts/graph-query.sh"
|
|
618
|
+
RECON_ISSUES=${RECON_ISSUES:-()}
|
|
516
619
|
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
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
|
-
|
|
647
|
+
### Blast Radius (downstream consumers of this change)
|
|
648
|
+
\`\`\`
|
|
649
|
+
$IMPACT_OUTPUT
|
|
650
|
+
\`\`\`
|
|
534
651
|
|
|
535
|
-
|
|
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
|
-
|
|
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 "
|
|
656
|
+
echo "[DRY-RUN] Would post FORGE:AUTOPILOT_IMPACT on issue #$issue"
|
|
549
657
|
fi
|
|
550
|
-
|
|
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
|
-
|
|
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. **
|
|
563
|
-
2. **
|
|
564
|
-
3. **
|
|
565
|
-
4. **
|
|
566
|
-
5. **
|
|
567
|
-
6. **
|
|
568
|
-
7. **
|
|
569
|
-
8. **
|
|
570
|
-
9. **
|
|
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
|
|
597
|
-
- **Token budget**: Recon is cheap (mostly
|
|
598
|
-
- **Idempotent**: Safe to run multiple times —
|
|
599
|
-
- **Pairs with /loop**: Run `/loop 4h /autopilot` for continuous improvement
|
|
600
|
-
- **Event-driven complement**: `/autopilot` is a periodic
|
|
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.
|