forgedock 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/engine/invariants.mjs +361 -0
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/journey.mjs +77 -7
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +540 -0
- package/commands/autopilot.md +959 -78
- package/commands/cleanup.md +52 -3
- package/commands/optimize.md +585 -0
- package/commands/orchestrate/phase-1-resolve.md +35 -25
- package/commands/orchestrate/phase-3-dependency.md +465 -34
- package/commands/orchestrate/phase-4-execution.md +448 -29
- package/commands/orchestrate/phase-6-report.md +198 -9
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/quality-gate.md +417 -0
- package/commands/review-pr.md +391 -13
- package/commands/spec-doctor.md +429 -0
- package/commands/validate.md +52 -0
- package/commands/work-on/build/architect.md +77 -0
- package/commands/work-on/build/context.md +357 -22
- package/commands/work-on/build/implement.md +2 -0
- package/commands/work-on/build/validate.md +28 -0
- package/commands/work-on/build.md +44 -0
- package/commands/work-on/close.md +621 -8
- package/commands/work-on/investigate.md +221 -0
- package/commands/work-on.md +15 -14
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/design/awesome-list-submissions.md +105 -0
- package/docs/design/f5bot-monitoring-setup.md +43 -0
- package/docs/design/growth-strategy-2026.md +579 -0
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +2 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/danger-zones.mjs +782 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/refresh-bot-token.sh +97 -0
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +157 -6
package/commands/autopilot.md
CHANGED
|
@@ -15,13 +15,16 @@ install: extras
|
|
|
15
15
|
- `{SERVER_SSH}` ← `services.server_ssh` (optional) — SSH target for production server health checks
|
|
16
16
|
- `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to ops work-item files
|
|
17
17
|
- `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data in Analytics Snapshot
|
|
18
|
+
- `{RECON_SOURCES}` ← `autopilot.recon_sources` (optional, default `["ci","backlog"]`) — list of collectors to run in Phase 1. Built-in tags: `ci` (Phase 1B), `backlog` (Phase 1C), `analytics` (Phase 1D). Omit the key to run `ci` and `backlog` only.
|
|
18
19
|
|
|
19
20
|
**NEVER use plan mode (EnterPlanMode).**
|
|
20
21
|
**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.
|
|
21
22
|
|
|
22
23
|
<!-- FORGE:SPEC_LOADED — autopilot.md loaded and active. Agent is bound by this spec. -->
|
|
23
24
|
|
|
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
|
|
25
|
+
You are a fully autonomous deploy loop for this project. Your job is to **detect the current pipeline state, work through all open issues, and deploy everything — without stopping for user confirmation**. Invoking `/autopilot` is the authorization to run to completion.
|
|
26
|
+
|
|
27
|
+
**Issue selection and priority**: Phase 2 (Fast Lane Loop) dispatches all eligible open issues **concurrently** via the durable engine — ordering is irrelevant at dispatch time because all issues start simultaneously. For scenarios requiring sequential, priority-ordered dispatch (P0 before P1 before P2, unlabeled excluded), use `scripts/select-fix-targets.sh` as the entry point. This script will be the substitution point when #1743 (economic scheduling) implements value/cost-based ordering. <!-- Added: forge#1752 -->
|
|
25
28
|
|
|
26
29
|
**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.
|
|
27
30
|
|
|
@@ -38,16 +41,19 @@ You are a fully autonomous deploy loop for this project. Your job is to **detect
|
|
|
38
41
|
| (none) | Full autonomous loop — state detection → recon → fast lane → milestone → report |
|
|
39
42
|
| `--dry-run` | Run all phases but do NOT create issues, merge PRs, or modify state — report only |
|
|
40
43
|
| `--recon-only` | Phase 0 (state detection) and Phase 1 (recon) only — no loop execution |
|
|
44
|
+
| `--fix` | Enable autonomous fix dispatch for this run (requires `autopilot.headless: true` in forge.yaml; refused otherwise) |
|
|
41
45
|
|
|
42
46
|
Parse `$ARGUMENTS` and set:
|
|
43
47
|
```bash
|
|
44
48
|
DRY_RUN=false
|
|
45
49
|
RECON_ONLY=false
|
|
50
|
+
AUTOPILOT_FIX=false
|
|
46
51
|
|
|
47
52
|
for arg in $ARGUMENTS; do
|
|
48
53
|
case "$arg" in
|
|
49
54
|
--dry-run) DRY_RUN=true ;;
|
|
50
55
|
--recon-only) RECON_ONLY=true ;;
|
|
56
|
+
--fix) AUTOPILOT_FIX=true ;;
|
|
51
57
|
esac
|
|
52
58
|
done
|
|
53
59
|
```
|
|
@@ -77,7 +83,96 @@ SERVER_SSH=$(yq '.services.server_ssh // ""' "$CONFIG_FILE" 2>/dev/null || echo
|
|
|
77
83
|
OPS_INBOX_PATH=$(yq '.services.ops_inbox_path // ""' "$CONFIG_FILE" 2>/dev/null || echo '')
|
|
78
84
|
BILLING_ENABLED=$(yq '.billing.enabled // false' "$CONFIG_FILE" 2>/dev/null || echo 'false')
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
# Hygiene counters — populated by Phase 1B.5 (/cleanup) and Phase 1A (/recover-orphans)
|
|
87
|
+
CLEANUP_LABELS_FIXED=0
|
|
88
|
+
|
|
89
|
+
# Autopilot ops issue — rolling issue where FORGE:AUTOPILOT_CYCLE annotations accumulate.
|
|
90
|
+
# Created automatically on first run if absent. Label is configurable; defaults to autopilot-ops.
|
|
91
|
+
AUTOPILOT_OPS_LABEL=$(yq '.autopilot.ops_issue_label // "autopilot-ops"' "$CONFIG_FILE" 2>/dev/null || echo 'autopilot-ops')
|
|
92
|
+
|
|
93
|
+
# Declared autonomy policy (forge.yaml → autopilot.*).
|
|
94
|
+
# All fields default to the conservative/safe value when absent so existing configs are unaffected.
|
|
95
|
+
AUTOPILOT_HEADLESS=$(yq '.autopilot.headless // false' "$CONFIG_FILE" 2>/dev/null || echo 'false')
|
|
96
|
+
AUTOPILOT_APPROVE_P0=$(yq '.autopilot.approve.p0 // "needs-human"' "$CONFIG_FILE" 2>/dev/null || echo 'needs-human')
|
|
97
|
+
AUTOPILOT_APPROVE_P1=$(yq '.autopilot.approve.p1 // "needs-human"' "$CONFIG_FILE" 2>/dev/null || echo 'needs-human')
|
|
98
|
+
AUTOPILOT_APPROVE_P2=$(yq '.autopilot.approve.p2 // "needs-human"' "$CONFIG_FILE" 2>/dev/null || echo 'needs-human')
|
|
99
|
+
AUTOPILOT_APPROVE_P3=$(yq '.autopilot.approve.p3 // "needs-human"' "$CONFIG_FILE" 2>/dev/null || echo 'needs-human')
|
|
100
|
+
BUDGET_PER_CYCLE_FIXES=$(yq '.autopilot.budget.per_cycle_fixes // "null"' "$CONFIG_FILE" 2>/dev/null || echo 'null')
|
|
101
|
+
# yq may emit a float (e.g. "5.0"); truncate to an integer so `[ "$FIX_COUNT" -ge ... ]`
|
|
102
|
+
# does not error. Leaves the "null"/empty guard value untouched. Single assignment, so
|
|
103
|
+
# both budget-ceiling checks (fast lane + milestone lane) read the truncated value.
|
|
104
|
+
case "$BUDGET_PER_CYCLE_FIXES" in
|
|
105
|
+
''|null) : ;;
|
|
106
|
+
*) BUDGET_PER_CYCLE_FIXES=$(echo "$BUDGET_PER_CYCLE_FIXES" | cut -d. -f1) ;;
|
|
107
|
+
esac
|
|
108
|
+
RECON_SOURCES=$(yq '.autopilot.recon_sources // ["ci","backlog"] | join(",")' "$CONFIG_FILE" 2>/dev/null || echo 'ci,backlog')
|
|
109
|
+
|
|
110
|
+
# FIX_COUNT tracks how many issues have been dispatched as autonomous fixes this cycle.
|
|
111
|
+
# Declared here (outside all loops) so it persists across Phase 2 and Phase 3 iterations.
|
|
112
|
+
FIX_COUNT=0
|
|
113
|
+
|
|
114
|
+
# Policy decision accumulators — populated during dispatch; written to FORGE:AUTOPILOT_CYCLE.
|
|
115
|
+
POLICY_APPROVED_ISSUES=() # issue numbers dispatched automatically by policy
|
|
116
|
+
POLICY_GATED_ISSUES=() # issue numbers whose plan was posted for async approval
|
|
117
|
+
POLICY_DEFERRED_ISSUES=() # issue numbers deferred due to budget ceiling
|
|
118
|
+
|
|
119
|
+
# Headless guard: if --fix requested but headless: false (default), refuse and exit.
|
|
120
|
+
# This guard runs after all config is read so the refusal message can include the fix target.
|
|
121
|
+
if [ "$AUTOPILOT_FIX" = "true" ] && [ "$AUTOPILOT_HEADLESS" != "true" ]; then
|
|
122
|
+
echo ""
|
|
123
|
+
echo "ERROR: --fix requested but autopilot.headless is not set to true in forge.yaml."
|
|
124
|
+
echo ""
|
|
125
|
+
echo "Autonomous fix dispatch requires an explicit opt-in:"
|
|
126
|
+
echo " autopilot:"
|
|
127
|
+
echo " headless: true"
|
|
128
|
+
echo " approve:"
|
|
129
|
+
echo " p2: auto # or whichever priorities you trust for autonomous fixing"
|
|
130
|
+
echo ""
|
|
131
|
+
echo "Without this declaration, /autopilot performs recon and reporting only — no fixes."
|
|
132
|
+
echo "Add the autopilot.headless: true block to forge.yaml to enable unattended fixing."
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Resolve or create the ops issue (idempotent — checks before creating)
|
|
137
|
+
OPS_ISSUE_NUMBER=$(gh issue list $GH_FLAG \
|
|
138
|
+
--state open \
|
|
139
|
+
--label "$AUTOPILOT_OPS_LABEL" \
|
|
140
|
+
--limit 1 \
|
|
141
|
+
--json number \
|
|
142
|
+
--jq '.[0].number // empty' 2>/dev/null || echo '')
|
|
143
|
+
|
|
144
|
+
if [ -z "$OPS_ISSUE_NUMBER" ]; then
|
|
145
|
+
echo "Autopilot: ops issue not found — creating one with label '$AUTOPILOT_OPS_LABEL'"
|
|
146
|
+
if [ "$DRY_RUN" = "false" ]; then
|
|
147
|
+
# Ensure the label exists before creating the issue
|
|
148
|
+
gh label create "$AUTOPILOT_OPS_LABEL" \
|
|
149
|
+
--description "Autopilot ops issue — rolling FORGE:AUTOPILOT_CYCLE annotations. Managed by /autopilot." \
|
|
150
|
+
--color "0075CA" \
|
|
151
|
+
$GH_FLAG 2>/dev/null || true
|
|
152
|
+
OPS_ISSUE_NUMBER=$(gh issue create $GH_FLAG \
|
|
153
|
+
--title "ops: autopilot cycle log" \
|
|
154
|
+
--label "$AUTOPILOT_OPS_LABEL" \
|
|
155
|
+
--body "$(cat <<'OPS_EOF'
|
|
156
|
+
## Autopilot Ops Issue
|
|
157
|
+
|
|
158
|
+
This issue is the rolling log for `/autopilot` cycle annotations. Each cycle appends a `<!-- FORGE:AUTOPILOT_CYCLE -->` comment with its metrics and phase-completion markers. Cycle N+1 reads the latest comment for baseline deltas and resume state.
|
|
159
|
+
|
|
160
|
+
**Do not close this issue manually.** It is managed by `/autopilot`.
|
|
161
|
+
OPS_EOF
|
|
162
|
+
)" \
|
|
163
|
+
--json number \
|
|
164
|
+
--jq '.number' 2>/dev/null || echo '')
|
|
165
|
+
echo "Autopilot: ops issue created: #$OPS_ISSUE_NUMBER"
|
|
166
|
+
else
|
|
167
|
+
echo "[DRY-RUN] Would create ops issue with label '$AUTOPILOT_OPS_LABEL'"
|
|
168
|
+
OPS_ISSUE_NUMBER="0"
|
|
169
|
+
fi
|
|
170
|
+
else
|
|
171
|
+
echo "Autopilot: ops issue found: #$OPS_ISSUE_NUMBER"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
echo "Autopilot: repo=$GH_REPO staging=$STAGING_BRANCH default=$DEFAULT_BRANCH dry_run=$DRY_RUN recon_only=$RECON_ONLY fix=$AUTOPILOT_FIX ops_issue=#$OPS_ISSUE_NUMBER"
|
|
175
|
+
echo "Autonomy policy: headless=$AUTOPILOT_HEADLESS approve={p0:$AUTOPILOT_APPROVE_P0,p1:$AUTOPILOT_APPROVE_P1,p2:$AUTOPILOT_APPROVE_P2,p3:$AUTOPILOT_APPROVE_P3} budget.per_cycle_fixes=$BUDGET_PER_CYCLE_FIXES"
|
|
81
176
|
```
|
|
82
177
|
|
|
83
178
|
---
|
|
@@ -172,6 +267,104 @@ echo "STATE: $OPEN_FAST_LANE open unmilestoned issue(s) for fast lane"
|
|
|
172
267
|
- If `STAGING_PR_NUMBER` is set → that deploy is in progress; `/deploy-pr` will detect and resume it
|
|
173
268
|
- Otherwise → proceed through phases in order
|
|
174
269
|
|
|
270
|
+
### 0G: Read prior cycle baseline (for delta computation)
|
|
271
|
+
|
|
272
|
+
Read the latest `FORGE:AUTOPILOT_CYCLE` annotation from the ops issue. Extract baseline metrics so Phase 4 can compute deltas showing what changed since the last cycle.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Read the latest AUTOPILOT_CYCLE annotation from the ops issue
|
|
276
|
+
PREV_CYCLE_COMMENT=""
|
|
277
|
+
PREV_CYCLE_ID=""
|
|
278
|
+
PREV_CYCLE_BASELINE=""
|
|
279
|
+
PREV_CYCLE_OPEN_ISSUES=""
|
|
280
|
+
PREV_CYCLE_CI_FAILURES=""
|
|
281
|
+
PREV_CYCLE_PENDING_FINDINGS=""
|
|
282
|
+
|
|
283
|
+
if [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ]; then
|
|
284
|
+
PREV_CYCLE_COMMENT=$(gh api repos/${GH_REPO}/issues/${OPS_ISSUE_NUMBER}/comments \
|
|
285
|
+
--jq '[.[] | select(.body | contains("FORGE:AUTOPILOT_CYCLE"))] | last | .body // ""' \
|
|
286
|
+
2>/dev/null || echo '')
|
|
287
|
+
|
|
288
|
+
if [ -n "$PREV_CYCLE_COMMENT" ]; then
|
|
289
|
+
# Check it's a complete (not interrupted) cycle
|
|
290
|
+
PREV_COMPLETE=$(echo "$PREV_CYCLE_COMMENT" | grep -c "FORGE:AUTOPILOT_CYCLE:COMPLETE" || echo "0")
|
|
291
|
+
|
|
292
|
+
if [ "${PREV_COMPLETE:-0}" -gt 0 ]; then
|
|
293
|
+
PREV_CYCLE_ID=$(echo "$PREV_CYCLE_COMMENT" \
|
|
294
|
+
| grep -oP '(?<=\*\*cycle_id\*\*: )[^\n]+' | head -1 | tr -d '[:space:]' || echo '')
|
|
295
|
+
PREV_CYCLE_BASELINE=$(echo "$PREV_CYCLE_COMMENT" \
|
|
296
|
+
| grep -oP '(?<=\*\*baseline\*\*: ).+' | head -1 || echo '{}')
|
|
297
|
+
# Extract numeric metrics from the baseline JSON string
|
|
298
|
+
PREV_CYCLE_OPEN_ISSUES=$(echo "$PREV_CYCLE_BASELINE" \
|
|
299
|
+
| jq -r '.open_issues // empty' 2>/dev/null || echo '')
|
|
300
|
+
PREV_CYCLE_CI_FAILURES=$(echo "$PREV_CYCLE_BASELINE" \
|
|
301
|
+
| jq -r '.ci_failures // empty' 2>/dev/null || echo '')
|
|
302
|
+
PREV_CYCLE_PENDING_FINDINGS=$(echo "$PREV_CYCLE_BASELINE" \
|
|
303
|
+
| jq -r '.pending_findings // empty' 2>/dev/null || echo '')
|
|
304
|
+
echo "Prior cycle: $PREV_CYCLE_ID — open_issues=$PREV_CYCLE_OPEN_ISSUES ci_failures=$PREV_CYCLE_CI_FAILURES findings=$PREV_CYCLE_PENDING_FINDINGS"
|
|
305
|
+
else
|
|
306
|
+
echo "Prior cycle annotation found but not complete — will check for resume in Phase 0H"
|
|
307
|
+
fi
|
|
308
|
+
else
|
|
309
|
+
echo "No prior FORGE:AUTOPILOT_CYCLE annotation found — this is the first cycle"
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 0H: Resume detection (skip committed phases on restart)
|
|
315
|
+
|
|
316
|
+
If the latest cycle annotation on the ops issue is **incomplete** (lacks `FORGE:AUTOPILOT_CYCLE:COMPLETE`), this cycle was interrupted. Read `phase_markers` to determine which phases already committed and skip them — avoiding duplicate issue creation, duplicate deploys, and redundant work.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
RESUME_FROM_PHASE=""
|
|
320
|
+
SKIP_RECON=false
|
|
321
|
+
SKIP_FAST_LANE=false
|
|
322
|
+
SKIP_MILESTONE=false
|
|
323
|
+
|
|
324
|
+
if [ -n "$PREV_CYCLE_COMMENT" ]; then
|
|
325
|
+
PREV_COMPLETE=$(echo "$PREV_CYCLE_COMMENT" | grep -c "FORGE:AUTOPILOT_CYCLE:COMPLETE" || echo "0")
|
|
326
|
+
|
|
327
|
+
if [ "${PREV_COMPLETE:-0}" -eq 0 ]; then
|
|
328
|
+
# Incomplete cycle — extract committed phase_markers
|
|
329
|
+
PREV_PHASE_MARKERS=$(echo "$PREV_CYCLE_COMMENT" \
|
|
330
|
+
| grep -oP '(?<=\*\*phase_markers\*\*: )[^\n]+' | head -1 | tr -d '[:space:]' || echo '')
|
|
331
|
+
|
|
332
|
+
echo "RESUME: Incomplete prior cycle detected. Committed phases: ${PREV_PHASE_MARKERS:-none}"
|
|
333
|
+
|
|
334
|
+
# Set skip flags for phases already committed
|
|
335
|
+
echo "$PREV_PHASE_MARKERS" | grep -q "recon" && SKIP_RECON=true && echo "RESUME: Skipping Phase 1 (recon already committed)"
|
|
336
|
+
echo "$PREV_PHASE_MARKERS" | grep -q "fast-lane" && SKIP_FAST_LANE=true && echo "RESUME: Skipping Phase 2 (fast-lane already committed)"
|
|
337
|
+
echo "$PREV_PHASE_MARKERS" | grep -q "milestone" && SKIP_MILESTONE=true && echo "RESUME: Skipping Phase 3 (milestone already committed)"
|
|
338
|
+
|
|
339
|
+
# Inherit the cycle_id from the interrupted cycle so the resume writes to the same annotation
|
|
340
|
+
RESUME_CYCLE_ID=$(echo "$PREV_CYCLE_COMMENT" \
|
|
341
|
+
| grep -oP '(?<=\*\*cycle_id\*\*: )[^\n]+' | head -1 | tr -d '[:space:]' || echo '')
|
|
342
|
+
[ -n "$RESUME_CYCLE_ID" ] && echo "RESUME: Continuing as cycle $RESUME_CYCLE_ID"
|
|
343
|
+
fi
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Generate a new cycle_id if not resuming an interrupted cycle
|
|
347
|
+
if [ -z "$RESUME_CYCLE_ID" ]; then
|
|
348
|
+
# Count completed cycles today to produce a unique counter
|
|
349
|
+
TODAY=$(date -u +%Y%m%d)
|
|
350
|
+
if [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ]; then
|
|
351
|
+
TODAY_CYCLE_COUNT=$(gh api repos/${GH_REPO}/issues/${OPS_ISSUE_NUMBER}/comments \
|
|
352
|
+
--jq "[.[] | select(.body | contains(\"FORGE:AUTOPILOT_CYCLE\") and contains(\"FORGE:AUTOPILOT_CYCLE:COMPLETE\") and contains(\"$TODAY\"))] | length" \
|
|
353
|
+
2>/dev/null || echo '0')
|
|
354
|
+
else
|
|
355
|
+
TODAY_CYCLE_COUNT=0
|
|
356
|
+
fi
|
|
357
|
+
CYCLE_COUNTER=$((TODAY_CYCLE_COUNT + 1))
|
|
358
|
+
CYCLE_ID="${TODAY}-${CYCLE_COUNTER}"
|
|
359
|
+
echo "New cycle: $CYCLE_ID"
|
|
360
|
+
else
|
|
361
|
+
CYCLE_ID="$RESUME_CYCLE_ID"
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Initialize phase_markers (will be extended as phases complete)
|
|
365
|
+
PHASE_MARKERS_COMMITTED="${PREV_PHASE_MARKERS:-}"
|
|
366
|
+
```
|
|
367
|
+
|
|
175
368
|
---
|
|
176
369
|
|
|
177
370
|
## Phase 1: Recon
|
|
@@ -180,6 +373,20 @@ echo "STATE: $OPEN_FAST_LANE open unmilestoned issue(s) for fast lane"
|
|
|
180
373
|
|
|
181
374
|
If `RECON_ONLY=true`, print the recon report and **stop after this phase**.
|
|
182
375
|
|
|
376
|
+
**Resume guard**: If `SKIP_RECON=true` (set in Phase 0H — recon was already committed in the interrupted cycle), skip Phase 1 entirely and proceed to Phase 2.
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
if [ "$SKIP_RECON" = "true" ]; then
|
|
380
|
+
echo "RESUME: Phase 1 (recon) already committed in cycle $CYCLE_ID — skipping"
|
|
381
|
+
RECON_ISSUES=()
|
|
382
|
+
RECENT_FAILURES=0
|
|
383
|
+
RECENT_RUNS=0
|
|
384
|
+
RECURRING=""
|
|
385
|
+
else
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
*(Close the skip block at the end of Phase 1, after the RECON_ONLY stop point.)*
|
|
389
|
+
|
|
183
390
|
### 1A: Recover orphaned pipeline state (MANDATORY — run before recon)
|
|
184
391
|
|
|
185
392
|
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.
|
|
@@ -203,70 +410,136 @@ fi
|
|
|
203
410
|
|
|
204
411
|
### 1B: CI/CD Health
|
|
205
412
|
|
|
413
|
+
Runs when `ci` is in `RECON_SOURCES` (default: always). Skip with `autopilot.recon_sources: ["backlog"]` in forge.yaml.
|
|
414
|
+
|
|
206
415
|
```bash
|
|
207
|
-
|
|
416
|
+
# Collector gate — skip if 'ci' not declared in RECON_SOURCES.
|
|
417
|
+
# Exact comma-delimited match so 'ci' does not false-match 'ci-extended' (grep -qw
|
|
418
|
+
# treats '-' as a word boundary and would match the hyphenated tag).
|
|
419
|
+
if echo "$RECON_SOURCES" | grep -qE '(^|,)ci(,|$)'; then
|
|
420
|
+
# Node.js is guaranteed by the npm installer; python3 is not (Windows alias issue)
|
|
421
|
+
DATE_1D_AGO=$(node -e "console.log(new Date(Date.now()-86400000).toISOString())")
|
|
422
|
+
|
|
423
|
+
# 3-minute timeout per collector — a hung gh call costs one report line, not the cycle
|
|
424
|
+
TIMEOUT_CMD=$(command -v timeout >/dev/null 2>&1 && echo "timeout 180" || echo "")
|
|
425
|
+
|
|
426
|
+
RECENT_FAILURES=$(${TIMEOUT_CMD} gh run list $GH_FLAG --limit 30 --json conclusion,createdAt,workflowName \
|
|
427
|
+
--jq "[.[] | select(.conclusion == \"failure\" and .createdAt > \"$DATE_1D_AGO\")] | length" \
|
|
428
|
+
2>/dev/null || echo "0")
|
|
429
|
+
|
|
430
|
+
RECENT_RUNS=$(${TIMEOUT_CMD} gh run list $GH_FLAG --limit 30 --json conclusion,createdAt \
|
|
431
|
+
--jq "[.[] | select(.createdAt > \"$DATE_1D_AGO\")] | length" \
|
|
432
|
+
2>/dev/null || echo "0")
|
|
433
|
+
|
|
434
|
+
echo "CI (24h): $RECENT_FAILURES/$RECENT_RUNS failures"
|
|
435
|
+
|
|
436
|
+
# Recurring failures (same workflow failing multiple times)
|
|
437
|
+
RECURRING=$(${TIMEOUT_CMD} gh run list $GH_FLAG --limit 30 --json conclusion,workflowName \
|
|
438
|
+
--jq '[.[] | select(.conclusion == "failure")] | group_by(.workflowName) | .[] | select(length > 1) | "\(length)x \(.[0].workflowName)"' \
|
|
439
|
+
2>/dev/null || echo '')
|
|
440
|
+
[ -n "$RECURRING" ] && echo "Recurring CI failures: $RECURRING"
|
|
441
|
+
else
|
|
442
|
+
echo "CI collector (1B): SKIPPED — 'ci' not in autopilot.recon_sources"
|
|
443
|
+
RECENT_FAILURES=0
|
|
444
|
+
RECENT_RUNS=0
|
|
445
|
+
RECURRING=""
|
|
446
|
+
fi
|
|
447
|
+
```
|
|
208
448
|
|
|
209
|
-
|
|
210
|
-
--jq "[.[] | select(.conclusion == \"failure\" and .createdAt > \"$DATE_1D_AGO\")] | length" \
|
|
211
|
-
2>/dev/null || echo "0")
|
|
449
|
+
### 1B.5: Label Hygiene Delegation
|
|
212
450
|
|
|
213
|
-
|
|
214
|
-
--jq "[.[] | select(.createdAt > \"$DATE_1D_AGO\")] | length" \
|
|
215
|
-
2>/dev/null || echo "0")
|
|
451
|
+
Sweep stale workflow labels from closed issues. Delegates to `/cleanup labels` — the command that verifies state before acting and maintains an audit trail. Autopilot never performs label mutations directly.
|
|
216
452
|
|
|
217
|
-
|
|
453
|
+
```bash
|
|
454
|
+
CLEANUP_AVAILABLE=$(ls ~/.claude/commands/cleanup.md 2>/dev/null && echo "true" || echo "false")
|
|
218
455
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
456
|
+
if [ "$DRY_RUN" = "false" ]; then
|
|
457
|
+
if [ "$CLEANUP_AVAILABLE" = "true" ]; then
|
|
458
|
+
echo "Running label hygiene sweep via /cleanup..."
|
|
459
|
+
Skill("cleanup", args="labels")
|
|
460
|
+
CLEANUP_LABELS_FIXED=1 # /cleanup ran; detailed counts are in its own output
|
|
461
|
+
else
|
|
462
|
+
echo "INFO: /cleanup not installed (extras tier) — skipping label hygiene sweep. Install with: npx forgedock install --extras"
|
|
463
|
+
fi
|
|
464
|
+
else
|
|
465
|
+
echo "[DRY-RUN] Would invoke: Skill(cleanup, labels)"
|
|
466
|
+
fi
|
|
224
467
|
```
|
|
225
468
|
|
|
226
469
|
### 1C: Issue Backlog Health
|
|
227
470
|
|
|
471
|
+
Runs when `backlog` is in `RECON_SOURCES` (default: always). Skip with `autopilot.recon_sources: ["ci"]` in forge.yaml.
|
|
472
|
+
|
|
228
473
|
```bash
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
--
|
|
242
|
-
(.labels | map(.name) | any(
|
|
243
|
-
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
474
|
+
# Collector gate — skip if 'backlog' not declared in RECON_SOURCES.
|
|
475
|
+
# Exact comma-delimited match (see 1B) — avoids false-matching hyphenated tags.
|
|
476
|
+
if echo "$RECON_SOURCES" | grep -qE '(^|,)backlog(,|$)'; then
|
|
477
|
+
# Node.js is guaranteed by the npm installer; python3 is not (Windows alias issue)
|
|
478
|
+
DATE_14D_AGO=$(node -e "console.log(new Date(Date.now()-14*86400000).toISOString())")
|
|
479
|
+
|
|
480
|
+
# Reuse TIMEOUT_CMD from 1B if set; detect if this is the first collector run
|
|
481
|
+
TIMEOUT_CMD=${TIMEOUT_CMD:-$(command -v timeout >/dev/null 2>&1 && echo "timeout 180" || echo "")}
|
|
482
|
+
|
|
483
|
+
# Count by priority
|
|
484
|
+
P0_COUNT=$(${TIMEOUT_CMD} gh issue list $GH_FLAG --state open --limit 200 --json labels \
|
|
485
|
+
--jq '[.[] | select(.labels | map(.name) | any(. == "P0"))] | length' 2>/dev/null || echo "0")
|
|
486
|
+
P1_COUNT=$(${TIMEOUT_CMD} gh issue list $GH_FLAG --state open --limit 200 --json labels \
|
|
487
|
+
--jq '[.[] | select(.labels | map(.name) | any(. == "P1"))] | length' 2>/dev/null || echo "0")
|
|
488
|
+
P2_COUNT=$(${TIMEOUT_CMD} gh issue list $GH_FLAG --state open --limit 200 --json labels \
|
|
489
|
+
--jq '[.[] | select(.labels | map(.name) | any(. == "P2"))] | length' 2>/dev/null || echo "0")
|
|
490
|
+
|
|
491
|
+
# Stale issues (open >14d, no workflow label, no milestone)
|
|
492
|
+
STALE_COUNT=$(${TIMEOUT_CMD} gh issue list $GH_FLAG --state open --limit 200 --json number,labels,createdAt,milestone \
|
|
493
|
+
--jq "[.[] | select(
|
|
494
|
+
(.labels | map(.name) | any(startswith(\"workflow:\")) | not) and
|
|
495
|
+
(.createdAt < \"$DATE_14D_AGO\") and
|
|
496
|
+
.milestone == null
|
|
497
|
+
)] | length" 2>/dev/null || echo "0")
|
|
498
|
+
|
|
499
|
+
echo "Backlog: P0=$P0_COUNT P1=$P1_COUNT P2=$P2_COUNT stale=$STALE_COUNT"
|
|
500
|
+
else
|
|
501
|
+
echo "Backlog collector (1C): SKIPPED — 'backlog' not in autopilot.recon_sources"
|
|
502
|
+
P0_COUNT=0; P1_COUNT=0; P2_COUNT=0; STALE_COUNT=0
|
|
503
|
+
fi
|
|
248
504
|
```
|
|
249
505
|
|
|
250
|
-
### 1D: Analytics Pulse (optional — forge.yaml-gated)
|
|
506
|
+
### 1D: Analytics Pulse (optional — forge.yaml-gated, recon_sources-gated)
|
|
507
|
+
|
|
508
|
+
Runs only when ALL of: (a) `analytics` is in `RECON_SOURCES`, (b) `CREDENTIALS_FILE` is set and exists, (c) the relevant MCP server is reachable. Enable with `autopilot.recon_sources: ["ci","backlog","analytics"]` in forge.yaml.
|
|
251
509
|
|
|
252
510
|
```bash
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
511
|
+
ANALYTICS_AVAILABLE=false
|
|
512
|
+
|
|
513
|
+
# Collector gate — skip if 'analytics' not declared in RECON_SOURCES.
|
|
514
|
+
# Exact comma-delimited match (see 1B) — avoids false-matching hyphenated tags.
|
|
515
|
+
if ! echo "$RECON_SOURCES" | grep -qE '(^|,)analytics(,|$)'; then
|
|
516
|
+
echo "Analytics collector (1D): SKIPPED — 'analytics' not in autopilot.recon_sources"
|
|
517
|
+
elif [ -z "$CREDENTIALS_FILE" ] || [ ! -f "$CREDENTIALS_FILE" ]; then
|
|
518
|
+
echo "Analytics collector (1D): SKIPPED — paths.credentials.file not set in forge.yaml or file absent"
|
|
259
519
|
else
|
|
260
|
-
echo "Analytics pulse:
|
|
261
|
-
|
|
520
|
+
echo "Analytics pulse: credentials at $CREDENTIALS_FILE — checking MCP availability..."
|
|
521
|
+
# MCP capability detection: verify the GSC MCP server is reachable before invoking.
|
|
522
|
+
# If the server is absent, the tool call would produce an agent error rather than a graceful skip.
|
|
523
|
+
# Detection strategy: attempt a lightweight probe (list tools or a no-op call) and gate on exit code.
|
|
524
|
+
# When the MCP server is present, proceed; when absent, log and skip without blocking the cycle.
|
|
525
|
+
# Dual-mode by design: an LLM agent interprets `mcp__gsc__search_analytics` as an MCP tool call
|
|
526
|
+
# (exit code = availability); a POSIX shell interprets it as an unknown command → 'command not
|
|
527
|
+
# found' → exit 1 → the else branch. Either interpretation yields a graceful skip when GSC is absent.
|
|
528
|
+
if mcp__gsc__search_analytics --list-only 2>/dev/null; then
|
|
529
|
+
ANALYTICS_AVAILABLE=true
|
|
530
|
+
echo "Analytics pulse: GSC MCP available — proceeding with 7-day search analytics query"
|
|
531
|
+
# Full GSC analytics query runs here when MCP is present
|
|
532
|
+
else
|
|
533
|
+
echo "Analytics pulse: SKIPPED — GSC MCP server not reachable (mcp__gsc__search_analytics unavailable)"
|
|
534
|
+
fi
|
|
262
535
|
fi
|
|
263
536
|
|
|
264
|
-
# Stripe balance: only if billing.enabled is true AND
|
|
537
|
+
# Stripe balance: only if billing.enabled is true AND analytics collector ran successfully
|
|
265
538
|
if [ "$BILLING_ENABLED" = "true" ] && [ "$ANALYTICS_AVAILABLE" = "true" ]; then
|
|
266
539
|
echo "Billing analytics: checking Stripe balance..."
|
|
267
540
|
# mcp__stripe__retrieve_balance — skip gracefully if MCP unavailable
|
|
268
541
|
else
|
|
269
|
-
echo "Billing analytics: SKIPPED — billing.enabled is false
|
|
542
|
+
echo "Billing analytics: SKIPPED — billing.enabled is false, credentials absent, or analytics collector skipped"
|
|
270
543
|
fi
|
|
271
544
|
```
|
|
272
545
|
|
|
@@ -274,19 +547,39 @@ fi
|
|
|
274
547
|
|
|
275
548
|
For each finding (recurring CI failures, critical stale issue patterns):
|
|
276
549
|
|
|
277
|
-
**Deduplication check first** —
|
|
550
|
+
**Deduplication check first** — deterministic script, then LLM fallback (canonical path from `issue.md §2D`):
|
|
278
551
|
```bash
|
|
279
|
-
|
|
280
|
-
|
|
552
|
+
# Authoritative deterministic check — uses token overlap algorithm (see scripts/issue-dedup.sh)
|
|
553
|
+
DEDUP_RESULT=$(scripts/issue-dedup.sh "{finding_title}" "$GH_FLAG" 2>&1)
|
|
554
|
+
DEDUP_EXIT=$?
|
|
555
|
+
|
|
556
|
+
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
557
|
+
echo "Near-duplicate detected: $DEDUP_RESULT"
|
|
558
|
+
echo "Existing issue found — skipping creation for this finding."
|
|
559
|
+
# STOP here for this finding — do not fall through to gh issue create
|
|
560
|
+
elif [ "$DEDUP_EXIT" -eq 2 ]; then
|
|
561
|
+
echo "Dedup check usage error: $DEDUP_RESULT"
|
|
562
|
+
echo "Do NOT proceed to issue creation — fix the invocation and retry."
|
|
563
|
+
# STOP here — do not fall through to gh issue create
|
|
564
|
+
fi
|
|
565
|
+
|
|
566
|
+
# If script exits 0 (no match), also run LLM-side semantic search as a secondary pass
|
|
567
|
+
if [ "$DEDUP_EXIT" -eq 0 ]; then
|
|
568
|
+
gh issue list $GH_FLAG --state open --limit 20 --search "{key_terms_from_finding}" \
|
|
569
|
+
--json number,title,labels \
|
|
570
|
+
--jq '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"' 2>/dev/null || true
|
|
571
|
+
# LLM: if the secondary pass finds a semantic duplicate, treat it as DEDUP_EXIT=1 and skip
|
|
572
|
+
fi
|
|
281
573
|
```
|
|
282
574
|
|
|
283
|
-
For new findings that have no existing open duplicate:
|
|
575
|
+
For new findings that have no existing open duplicate (DEDUP_EXIT=0 and no LLM match):
|
|
284
576
|
```bash
|
|
285
|
-
if [ "$DRY_RUN" = "false" ]; then
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
577
|
+
if [ "$DRY_RUN" = "false" ] && [ "${DEDUP_EXIT:-0}" -eq 0 ]; then
|
|
578
|
+
# Write body to a temp file to avoid heredoc shell-expansion and injection issues
|
|
579
|
+
BODY_TMPFILE=$(mktemp /tmp/autopilot-issue-body-XXXXXX.md)
|
|
580
|
+
trap 'rm -f "$BODY_TMPFILE"' EXIT
|
|
581
|
+
|
|
582
|
+
cat > "$BODY_TMPFILE" <<'ISSUE_BODY_EOF'
|
|
290
583
|
## Problem
|
|
291
584
|
|
|
292
585
|
{Description of the finding with specific data points.}
|
|
@@ -297,7 +590,7 @@ if [ "$DRY_RUN" = "false" ]; then
|
|
|
297
590
|
|
|
298
591
|
## Affected Files
|
|
299
592
|
|
|
300
|
-
Files that need changes:
|
|
593
|
+
Files that need changes (ordered by dependency):
|
|
301
594
|
1. `{filepath}` — {what needs to change}
|
|
302
595
|
|
|
303
596
|
## Acceptance Criteria
|
|
@@ -306,14 +599,57 @@ Files that need changes:
|
|
|
306
599
|
|
|
307
600
|
## Context
|
|
308
601
|
|
|
309
|
-
Found by
|
|
602
|
+
Found by `/autopilot` cycle on {CYCLE_TIMESTAMP}.
|
|
310
603
|
|
|
311
604
|
## Evidence
|
|
312
605
|
|
|
313
606
|
{Concrete data — log lines, failure counts, metrics}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
607
|
+
ISSUE_BODY_EOF
|
|
608
|
+
|
|
609
|
+
# Substitute the cycle timestamp (not inside heredoc to avoid expansion-in-template issues)
|
|
610
|
+
sed -i "s|{CYCLE_TIMESTAMP}|$(date -u +%Y-%m-%dT%H:%M:%SZ)|g" "$BODY_TMPFILE"
|
|
611
|
+
|
|
612
|
+
NEW_NUMBER=$(gh issue create $GH_FLAG \
|
|
613
|
+
--title "fix: {finding_description}" \
|
|
614
|
+
--label "P2,bug" \
|
|
615
|
+
--body-file "$BODY_TMPFILE" \
|
|
616
|
+
--json number --jq '.number' 2>/dev/null)
|
|
617
|
+
|
|
618
|
+
rm -f "$BODY_TMPFILE"
|
|
619
|
+
trap - EXIT
|
|
620
|
+
|
|
621
|
+
# §4C.5: Body validation/repair — ensure mandatory pipeline sections are present
|
|
622
|
+
# (canonical pattern from issue.md §4C.5 — never fails/blocks, only repairs)
|
|
623
|
+
if [ -n "$NEW_NUMBER" ]; then
|
|
624
|
+
CREATED_BODY=$(gh issue view "$NEW_NUMBER" $GH_FLAG --json body --jq '.body' 2>/dev/null || echo '')
|
|
625
|
+
MISSING_SECTIONS=""
|
|
626
|
+
echo "$CREATED_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
|
|
627
|
+
echo "$CREATED_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
|
|
628
|
+
echo "$CREATED_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
|
|
629
|
+
|
|
630
|
+
if [ -n "$MISSING_SECTIONS" ]; then
|
|
631
|
+
echo "WARNING: Issue #$NEW_NUMBER body is missing sections:$MISSING_SECTIONS — adding placeholders"
|
|
632
|
+
APPEND_TEXT=""
|
|
633
|
+
echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
|
|
634
|
+
## Problem
|
|
635
|
+
|
|
636
|
+
Root cause unknown — investigation needed."
|
|
637
|
+
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
638
|
+
## Affected Files
|
|
639
|
+
|
|
640
|
+
Files to be identified during investigation."
|
|
641
|
+
echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
|
|
642
|
+
## Acceptance Criteria
|
|
643
|
+
|
|
644
|
+
- [ ] Fix confirmed during investigation."
|
|
645
|
+
REPAIRED_BODY="${CREATED_BODY}${APPEND_TEXT}"
|
|
646
|
+
gh issue edit "$NEW_NUMBER" $GH_FLAG --body "$REPAIRED_BODY"
|
|
647
|
+
echo "Body repaired — added:$MISSING_SECTIONS"
|
|
648
|
+
else
|
|
649
|
+
echo "Body validation passed for #$NEW_NUMBER — all mandatory sections present"
|
|
650
|
+
fi
|
|
651
|
+
fi
|
|
652
|
+
elif [ "$DRY_RUN" = "true" ]; then
|
|
317
653
|
echo "[DRY-RUN] Would create issue: fix: {finding_description}"
|
|
318
654
|
fi
|
|
319
655
|
```
|
|
@@ -339,12 +675,21 @@ If `RECON_ONLY=true`, print recon report and **stop here**:
|
|
|
339
675
|
- Stale (>14d, no workflow): $STALE_COUNT
|
|
340
676
|
|
|
341
677
|
### Actions Taken
|
|
342
|
-
- Orphans recovered: $INFLIGHT_ISSUES
|
|
678
|
+
- Orphans recovered (/recover-orphans): $INFLIGHT_ISSUES
|
|
679
|
+
- Label hygiene (/cleanup labels): $([ "${CLEANUP_LABELS_FIXED:-0}" -eq 1 ] && echo "ran" || echo "skipped (not installed or dry-run)")
|
|
343
680
|
- Issues created from recon: ${#RECON_ISSUES[@]}
|
|
344
681
|
|
|
345
682
|
Run without --recon-only to execute the full autonomous loop.
|
|
346
683
|
```
|
|
347
684
|
|
|
685
|
+
```bash
|
|
686
|
+
fi # end SKIP_RECON guard
|
|
687
|
+
|
|
688
|
+
# Phase 1 complete — record phase marker
|
|
689
|
+
PHASE_MARKERS_COMMITTED="${PHASE_MARKERS_COMMITTED:+$PHASE_MARKERS_COMMITTED,}recon"
|
|
690
|
+
echo "Phase 1 complete. Committed phases: $PHASE_MARKERS_COMMITTED"
|
|
691
|
+
```
|
|
692
|
+
|
|
348
693
|
---
|
|
349
694
|
|
|
350
695
|
## Phase 2: Fast Lane Loop
|
|
@@ -353,11 +698,19 @@ Run without --recon-only to execute the full autonomous loop.
|
|
|
353
698
|
|
|
354
699
|
**Overrides "never merge to main"**: This phase deploys staging→main after each iteration. `/autopilot` is the authorized deploy system.
|
|
355
700
|
|
|
701
|
+
**Resume guard**: If `SKIP_FAST_LANE=true` (set in Phase 0H — fast lane was already committed in the interrupted cycle), skip Phase 2 entirely and proceed to Phase 3.
|
|
702
|
+
|
|
356
703
|
```bash
|
|
357
704
|
FAST_LANE_ITERATIONS=0
|
|
358
705
|
MAX_FAST_LANE_ITERATIONS=20 # safety cap — prevents infinite loop on stuck state
|
|
359
706
|
FAST_LANE_DEPLOYS=0
|
|
360
707
|
FAST_LANE_FINDINGS_BOUNCED=0
|
|
708
|
+
DISPATCHED_ISSUES=() # accumulates every issue number dispatched this cycle (for Phase 4 report)
|
|
709
|
+
STALLED_ISSUES=() # issues that did not reach a terminal state after dispatch
|
|
710
|
+
|
|
711
|
+
if [ "$SKIP_FAST_LANE" = "true" ]; then
|
|
712
|
+
echo "RESUME: Phase 2 (fast-lane) already committed in cycle $CYCLE_ID — skipping"
|
|
713
|
+
else
|
|
361
714
|
|
|
362
715
|
echo "=== Fast Lane Loop ==="
|
|
363
716
|
|
|
@@ -398,8 +751,11 @@ while true; do
|
|
|
398
751
|
|
|
399
752
|
if [ "$DRY_RUN" = "false" ]; then
|
|
400
753
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
401
|
-
# Engine-first dispatch: enumerate issues and run each
|
|
402
|
-
#
|
|
754
|
+
# Engine-first dispatch: enumerate ALL eligible fast-lane issues and run each
|
|
755
|
+
# through the durable engine concurrently (background workers + wait).
|
|
756
|
+
# Dispatch order is irrelevant here — all issues start simultaneously and the
|
|
757
|
+
# engine's per-issue leases prevent conflicts. For sequential priority-ordered
|
|
758
|
+
# dispatch, use scripts/select-fix-targets.sh instead. <!-- forge#1752 -->
|
|
403
759
|
FAST_LANE_ISSUE_NUMS=$(gh issue list $GH_FLAG \
|
|
404
760
|
--state open \
|
|
405
761
|
--limit 200 \
|
|
@@ -411,16 +767,156 @@ while true; do
|
|
|
411
767
|
2>/dev/null || echo '')
|
|
412
768
|
|
|
413
769
|
for ISSUE_NUM in $FAST_LANE_ISSUE_NUMS; do
|
|
770
|
+
# Autonomy policy gate — evaluate approve map by issue priority.
|
|
771
|
+
# Only applies when --fix is set; without --fix all issues flow normally.
|
|
772
|
+
if [ "$AUTOPILOT_FIX" = "true" ]; then
|
|
773
|
+
# Read the priority label for this issue (format: priority:P0, priority:P1, etc.)
|
|
774
|
+
ISSUE_PRIORITY_LABEL=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
775
|
+
--json labels --jq '.labels | map(.name) | map(select(startswith("priority:"))) | .[0] // ""' \
|
|
776
|
+
2>/dev/null || echo '')
|
|
777
|
+
# Normalize: "priority:P2" → "p2"; unlabeled → "p2" (treat as P2 for policy default)
|
|
778
|
+
PRIORITY_KEY=$(echo "$ISSUE_PRIORITY_LABEL" | sed 's/priority://;s/P/p/' | tr '[:upper:]' '[:lower:]')
|
|
779
|
+
[ -z "$PRIORITY_KEY" ] && PRIORITY_KEY="p2"
|
|
780
|
+
|
|
781
|
+
# Resolve approval action from the map
|
|
782
|
+
case "$PRIORITY_KEY" in
|
|
783
|
+
p0) APPROVE_ACTION="$AUTOPILOT_APPROVE_P0" ;;
|
|
784
|
+
p1) APPROVE_ACTION="$AUTOPILOT_APPROVE_P1" ;;
|
|
785
|
+
p2) APPROVE_ACTION="$AUTOPILOT_APPROVE_P2" ;;
|
|
786
|
+
p3) APPROVE_ACTION="$AUTOPILOT_APPROVE_P3" ;;
|
|
787
|
+
*) APPROVE_ACTION="needs-human" ;; # unknown priority → gate
|
|
788
|
+
esac
|
|
789
|
+
|
|
790
|
+
# Budget ceiling check (only for auto-approved issues)
|
|
791
|
+
if [ "$APPROVE_ACTION" = "auto" ]; then
|
|
792
|
+
if [ "$BUDGET_PER_CYCLE_FIXES" != "null" ] && [ -n "$BUDGET_PER_CYCLE_FIXES" ]; then
|
|
793
|
+
if [ "$FIX_COUNT" -ge "$BUDGET_PER_CYCLE_FIXES" ]; then
|
|
794
|
+
echo " #$ISSUE_NUM deferred (budget ceiling: $FIX_COUNT/$BUDGET_PER_CYCLE_FIXES fixes dispatched)"
|
|
795
|
+
POLICY_DEFERRED_ISSUES+=("$ISSUE_NUM")
|
|
796
|
+
continue
|
|
797
|
+
fi
|
|
798
|
+
fi
|
|
799
|
+
fi
|
|
800
|
+
|
|
801
|
+
if [ "$APPROVE_ACTION" = "auto" ]; then
|
|
802
|
+
echo " #$ISSUE_NUM approved-by-policy ($PRIORITY_KEY → auto) — dispatching"
|
|
803
|
+
POLICY_APPROVED_ISSUES+=("$ISSUE_NUM")
|
|
804
|
+
FIX_COUNT=$((FIX_COUNT + 1))
|
|
805
|
+
else
|
|
806
|
+
# needs-human: post the fix plan on the ops issue for async approval; never dispatch
|
|
807
|
+
echo " #$ISSUE_NUM gated-by-policy ($PRIORITY_KEY → needs-human) — posting plan to ops issue #$OPS_ISSUE_NUMBER"
|
|
808
|
+
POLICY_GATED_ISSUES+=("$ISSUE_NUM")
|
|
809
|
+
ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" $GH_FLAG --json title --jq '.title' 2>/dev/null || echo "#$ISSUE_NUM")
|
|
810
|
+
if [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ]; then
|
|
811
|
+
gh issue comment "$OPS_ISSUE_NUMBER" $GH_FLAG --body "<!-- FORGE:AUTOPILOT_GATE -->
|
|
812
|
+
## Gated Fix — Requires Human Approval
|
|
813
|
+
|
|
814
|
+
**Issue**: #${ISSUE_NUM} — ${ISSUE_TITLE}
|
|
815
|
+
**Priority**: \`${ISSUE_PRIORITY_LABEL:-unlabeled}\`
|
|
816
|
+
**Policy**: \`autopilot.approve.${PRIORITY_KEY} = needs-human\`
|
|
817
|
+
**Action needed**: Review #${ISSUE_NUM} and either approve for autonomous dispatch or resolve manually.
|
|
818
|
+
|
|
819
|
+
To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopilot --fix\`, OR change \`autopilot.approve.${PRIORITY_KEY}\` to \`auto\` in forge.yaml.
|
|
820
|
+
|
|
821
|
+
<!-- FORGE:AUTOPILOT_GATE:COMPLETE -->" 2>/dev/null || true
|
|
822
|
+
fi
|
|
823
|
+
continue # skip dispatch for gated issues
|
|
824
|
+
fi
|
|
825
|
+
fi
|
|
826
|
+
|
|
414
827
|
echo "Dispatching #$ISSUE_NUM via forgedock run-issue --lane staging"
|
|
828
|
+
DISPATCHED_ISSUES+=("$ISSUE_NUM")
|
|
415
829
|
forgedock run-issue "$ISSUE_NUM" --lane staging &
|
|
416
830
|
done
|
|
417
831
|
|
|
418
832
|
# Wait for all engine workers to complete before proceeding to deploy step.
|
|
419
833
|
wait
|
|
420
834
|
echo "Engine dispatch complete for fast-lane iteration $FAST_LANE_ITERATIONS"
|
|
835
|
+
|
|
836
|
+
# Terminal-state verification sweep (MANDATORY after every dispatch batch).
|
|
837
|
+
# Query actual GitHub label state for each dispatched issue — the engine's
|
|
838
|
+
# exit code only reflects process completion, not pipeline terminal state.
|
|
839
|
+
# An issue whose PR merged but whose close phase was interrupted will show
|
|
840
|
+
# workflow:in-review (non-terminal) here and be flagged for targeted recovery.
|
|
841
|
+
echo "Verifying terminal state for $( echo "$FAST_LANE_ISSUE_NUMS" | wc -w | tr -d ' ') dispatched issue(s)..."
|
|
842
|
+
for ISSUE_NUM in $FAST_LANE_ISSUE_NUMS; do
|
|
843
|
+
ISSUE_LABELS=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
844
|
+
--json labels,state \
|
|
845
|
+
--jq '(.labels | map(.name) | join(",")) + "|" + .state' \
|
|
846
|
+
2>/dev/null || echo "unknown|unknown")
|
|
847
|
+
ISSUE_LABEL_STR="${ISSUE_LABELS%%|*}"
|
|
848
|
+
ISSUE_STATE="${ISSUE_LABELS##*|}"
|
|
849
|
+
|
|
850
|
+
if echo "$ISSUE_LABEL_STR" | grep -qE 'workflow:merged|workflow:invalid|needs-human' || [ "$ISSUE_STATE" = "CLOSED" ]; then
|
|
851
|
+
echo " #$ISSUE_NUM ✓ terminal ($ISSUE_LABEL_STR)"
|
|
852
|
+
elif echo "$ISSUE_LABEL_STR" | grep -q 'workflow:in-review'; then
|
|
853
|
+
# Known recoverable stall class: PR merged but close phase did not run.
|
|
854
|
+
# Verify PR state before invoking recover-orphans.
|
|
855
|
+
# Search by issue number — ForgeDock branch slugs include the issue number,
|
|
856
|
+
# so a title-only --head slug lookup never matches (returned dead-empty).
|
|
857
|
+
MERGED_PR=$(gh pr list $GH_FLAG --search "closes #$ISSUE_NUM" \
|
|
858
|
+
--state merged --limit 1 --json number --jq '.[0].number' 2>/dev/null || echo '')
|
|
859
|
+
if [ -n "$MERGED_PR" ] || gh api repos/$GH_REPO/issues/$ISSUE_NUM/events \
|
|
860
|
+
--jq '[.[] | select(.event == "labeled" and .label.name == "workflow:merged")] | length > 0' \
|
|
861
|
+
2>/dev/null | grep -q true; then
|
|
862
|
+
echo " #$ISSUE_NUM ⟳ stalled at workflow:in-review (PR merged) — resuming via recover-orphans"
|
|
863
|
+
if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ]; then
|
|
864
|
+
Skill("recover-orphans", args="--issue $ISSUE_NUM")
|
|
865
|
+
else
|
|
866
|
+
echo " WARNING: recover-orphans not installed — cannot auto-resume #$ISSUE_NUM"
|
|
867
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
868
|
+
fi
|
|
869
|
+
else
|
|
870
|
+
echo " #$ISSUE_NUM ⚠ non-terminal ($ISSUE_LABEL_STR) — will retry next iteration"
|
|
871
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
872
|
+
fi
|
|
873
|
+
else
|
|
874
|
+
echo " #$ISSUE_NUM ⚠ non-terminal ($ISSUE_LABEL_STR) — will retry next iteration"
|
|
875
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
876
|
+
fi
|
|
877
|
+
done
|
|
421
878
|
else
|
|
422
|
-
echo "
|
|
879
|
+
echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
|
|
880
|
+
# Safety Rule 2 (needs-human guarantee): The engine-first path above filters
|
|
881
|
+
# needs-human issues via an explicit jq selector. On this fallback path,
|
|
882
|
+
# /orchestrate enforces the same guarantee at its own phase entry — it skips
|
|
883
|
+
# any issue labeled needs-human before dispatching /work-on. Both paths
|
|
884
|
+
# implement Rule 2; the mechanism differs (jq filter vs. downstream check).
|
|
885
|
+
|
|
886
|
+
# Snapshot the fast-lane issue list before orchestrate runs so we can verify
|
|
887
|
+
# terminal state afterward. orchestrate enumerates internally, so we cannot
|
|
888
|
+
# rely on FAST_LANE_ISSUE_NUMS (which was not set on this path).
|
|
889
|
+
PRE_DISPATCH_ISSUES=$(gh issue list $GH_FLAG \
|
|
890
|
+
--state open \
|
|
891
|
+
--limit 200 \
|
|
892
|
+
--json number,milestone,labels \
|
|
893
|
+
--jq '[.[] | select(
|
|
894
|
+
.milestone == null and
|
|
895
|
+
(.labels | map(.name) | any(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:decomposed" or . == "needs-human") | not)
|
|
896
|
+
)] | .[].number' \
|
|
897
|
+
2>/dev/null || echo '')
|
|
898
|
+
for N in $PRE_DISPATCH_ISSUES; do DISPATCHED_ISSUES+=("$N"); done
|
|
899
|
+
|
|
423
900
|
Skill("orchestrate", args="fast-lane")
|
|
901
|
+
|
|
902
|
+
# Terminal-state verification after orchestrate (fallback path).
|
|
903
|
+
# Re-query GitHub state for each pre-dispatch issue.
|
|
904
|
+
echo "Verifying terminal state for $( echo "$PRE_DISPATCH_ISSUES" | wc -w | tr -d ' ') dispatched issue(s) (orchestrate path)..."
|
|
905
|
+
for ISSUE_NUM in $PRE_DISPATCH_ISSUES; do
|
|
906
|
+
ISSUE_LABELS=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
907
|
+
--json labels,state \
|
|
908
|
+
--jq '(.labels | map(.name) | join(",")) + "|" + .state' \
|
|
909
|
+
2>/dev/null || echo "unknown|unknown")
|
|
910
|
+
ISSUE_LABEL_STR="${ISSUE_LABELS%%|*}"
|
|
911
|
+
ISSUE_STATE="${ISSUE_LABELS##*|}"
|
|
912
|
+
|
|
913
|
+
if echo "$ISSUE_LABEL_STR" | grep -qE 'workflow:merged|workflow:invalid|needs-human' || [ "$ISSUE_STATE" = "CLOSED" ]; then
|
|
914
|
+
echo " #$ISSUE_NUM ✓ terminal ($ISSUE_LABEL_STR)"
|
|
915
|
+
else
|
|
916
|
+
echo " #$ISSUE_NUM ⚠ non-terminal ($ISSUE_LABEL_STR)"
|
|
917
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
918
|
+
fi
|
|
919
|
+
done
|
|
424
920
|
fi
|
|
425
921
|
else
|
|
426
922
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
@@ -469,6 +965,12 @@ while true; do
|
|
|
469
965
|
done
|
|
470
966
|
|
|
471
967
|
echo "Fast lane complete: $FAST_LANE_ITERATIONS iteration(s), $FAST_LANE_DEPLOYS deploy(s)"
|
|
968
|
+
|
|
969
|
+
fi # end SKIP_FAST_LANE guard
|
|
970
|
+
|
|
971
|
+
# Phase 2 complete — record phase marker
|
|
972
|
+
PHASE_MARKERS_COMMITTED="${PHASE_MARKERS_COMMITTED:+$PHASE_MARKERS_COMMITTED,}fast-lane"
|
|
973
|
+
echo "Phase 2 complete. Committed phases: $PHASE_MARKERS_COMMITTED"
|
|
472
974
|
```
|
|
473
975
|
|
|
474
976
|
---
|
|
@@ -479,10 +981,16 @@ echo "Fast lane complete: $FAST_LANE_ITERATIONS iteration(s), $FAST_LANE_DEPLOYS
|
|
|
479
981
|
|
|
480
982
|
**Each milestone cycle**: orchestrate all open issues in the milestone → ship milestone branch to staging → deploy staging to main.
|
|
481
983
|
|
|
984
|
+
**Resume guard**: If `SKIP_MILESTONE=true` (set in Phase 0H — milestone loop was already committed in the interrupted cycle), skip Phase 3 entirely and proceed to Phase 4.
|
|
985
|
+
|
|
482
986
|
```bash
|
|
483
987
|
MILESTONE_ITERATIONS=0
|
|
484
988
|
MILESTONE_DEPLOYS=0
|
|
485
989
|
|
|
990
|
+
if [ "$SKIP_MILESTONE" = "true" ]; then
|
|
991
|
+
echo "RESUME: Phase 3 (milestone) already committed in cycle $CYCLE_ID — skipping"
|
|
992
|
+
else
|
|
993
|
+
|
|
486
994
|
echo "=== Milestone Loop ==="
|
|
487
995
|
|
|
488
996
|
# Get all open milestones with completion percentages
|
|
@@ -503,7 +1011,9 @@ echo "Found $MILESTONE_COUNT milestone(s)"
|
|
|
503
1011
|
echo "$MILESTONES" | jq -r '.[] | "\(.completion_pct)% — \(.title) (\(.open_issues) open, \(.closed_issues) closed)"'
|
|
504
1012
|
|
|
505
1013
|
# Process each milestone — sorted by completion% descending, skip 0%
|
|
506
|
-
|
|
1014
|
+
# Process substitution (not a pipe) so DISPATCHED_ISSUES+=() appends in the loop
|
|
1015
|
+
# body run in THIS shell, not a subshell, and survive for the Phase 4 report.
|
|
1016
|
+
while IFS= read -r milestone; do
|
|
507
1017
|
MS_TITLE=$(echo "$milestone" | jq -r '.title')
|
|
508
1018
|
MS_SLUG=$(echo "$milestone" | jq -r '.slug')
|
|
509
1019
|
MS_OPEN=$(echo "$milestone" | jq -r '.open_issues')
|
|
@@ -537,15 +1047,129 @@ echo "$MILESTONES" | jq -c '.[]' | while IFS= read -r milestone; do
|
|
|
537
1047
|
2>/dev/null || echo '')
|
|
538
1048
|
|
|
539
1049
|
for ISSUE_NUM in $MS_ISSUE_NUMS; do
|
|
1050
|
+
# Autonomy policy gate — same approve map as Phase 2 fast lane.
|
|
1051
|
+
# Only applies when --fix is set; without --fix all issues flow normally.
|
|
1052
|
+
if [ "$AUTOPILOT_FIX" = "true" ]; then
|
|
1053
|
+
ISSUE_PRIORITY_LABEL=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
1054
|
+
--json labels --jq '.labels | map(.name) | map(select(startswith("priority:"))) | .[0] // ""' \
|
|
1055
|
+
2>/dev/null || echo '')
|
|
1056
|
+
PRIORITY_KEY=$(echo "$ISSUE_PRIORITY_LABEL" | sed 's/priority://;s/P/p/' | tr '[:upper:]' '[:lower:]')
|
|
1057
|
+
[ -z "$PRIORITY_KEY" ] && PRIORITY_KEY="p2"
|
|
1058
|
+
|
|
1059
|
+
case "$PRIORITY_KEY" in
|
|
1060
|
+
p0) APPROVE_ACTION="$AUTOPILOT_APPROVE_P0" ;;
|
|
1061
|
+
p1) APPROVE_ACTION="$AUTOPILOT_APPROVE_P1" ;;
|
|
1062
|
+
p2) APPROVE_ACTION="$AUTOPILOT_APPROVE_P2" ;;
|
|
1063
|
+
p3) APPROVE_ACTION="$AUTOPILOT_APPROVE_P3" ;;
|
|
1064
|
+
*) APPROVE_ACTION="needs-human" ;;
|
|
1065
|
+
esac
|
|
1066
|
+
|
|
1067
|
+
if [ "$APPROVE_ACTION" = "auto" ]; then
|
|
1068
|
+
if [ "$BUDGET_PER_CYCLE_FIXES" != "null" ] && [ -n "$BUDGET_PER_CYCLE_FIXES" ]; then
|
|
1069
|
+
if [ "$FIX_COUNT" -ge "$BUDGET_PER_CYCLE_FIXES" ]; then
|
|
1070
|
+
echo " #$ISSUE_NUM deferred (budget ceiling: $FIX_COUNT/$BUDGET_PER_CYCLE_FIXES fixes dispatched)"
|
|
1071
|
+
POLICY_DEFERRED_ISSUES+=("$ISSUE_NUM")
|
|
1072
|
+
continue
|
|
1073
|
+
fi
|
|
1074
|
+
fi
|
|
1075
|
+
fi
|
|
1076
|
+
|
|
1077
|
+
if [ "$APPROVE_ACTION" = "auto" ]; then
|
|
1078
|
+
echo " #$ISSUE_NUM approved-by-policy ($PRIORITY_KEY → auto) — dispatching"
|
|
1079
|
+
POLICY_APPROVED_ISSUES+=("$ISSUE_NUM")
|
|
1080
|
+
FIX_COUNT=$((FIX_COUNT + 1))
|
|
1081
|
+
else
|
|
1082
|
+
echo " #$ISSUE_NUM gated-by-policy ($PRIORITY_KEY → needs-human) — posting plan to ops issue #$OPS_ISSUE_NUMBER"
|
|
1083
|
+
POLICY_GATED_ISSUES+=("$ISSUE_NUM")
|
|
1084
|
+
ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" $GH_FLAG --json title --jq '.title' 2>/dev/null || echo "#$ISSUE_NUM")
|
|
1085
|
+
if [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ]; then
|
|
1086
|
+
gh issue comment "$OPS_ISSUE_NUMBER" $GH_FLAG --body "<!-- FORGE:AUTOPILOT_GATE -->
|
|
1087
|
+
## Gated Fix — Requires Human Approval
|
|
1088
|
+
|
|
1089
|
+
**Issue**: #${ISSUE_NUM} — ${ISSUE_TITLE}
|
|
1090
|
+
**Priority**: \`${ISSUE_PRIORITY_LABEL:-unlabeled}\`
|
|
1091
|
+
**Policy**: \`autopilot.approve.${PRIORITY_KEY} = needs-human\`
|
|
1092
|
+
**Action needed**: Review #${ISSUE_NUM} and either approve for autonomous dispatch or resolve manually.
|
|
1093
|
+
|
|
1094
|
+
To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopilot --fix\`, OR change \`autopilot.approve.${PRIORITY_KEY}\` to \`auto\` in forge.yaml.
|
|
1095
|
+
|
|
1096
|
+
<!-- FORGE:AUTOPILOT_GATE:COMPLETE -->" 2>/dev/null || true
|
|
1097
|
+
fi
|
|
1098
|
+
continue # skip dispatch for gated issues
|
|
1099
|
+
fi
|
|
1100
|
+
fi
|
|
1101
|
+
|
|
540
1102
|
echo "Dispatching #$ISSUE_NUM via forgedock run-issue --lane milestone/$MS_SLUG"
|
|
1103
|
+
DISPATCHED_ISSUES+=("$ISSUE_NUM")
|
|
541
1104
|
forgedock run-issue "$ISSUE_NUM" --lane "milestone/$MS_SLUG" &
|
|
542
1105
|
done
|
|
543
1106
|
|
|
544
1107
|
wait
|
|
545
1108
|
echo "Engine dispatch complete for milestone '$MS_TITLE'"
|
|
1109
|
+
|
|
1110
|
+
# Terminal-state verification sweep for milestone batch.
|
|
1111
|
+
echo "Verifying terminal state for $( echo "$MS_ISSUE_NUMS" | wc -w | tr -d ' ') dispatched milestone issue(s)..."
|
|
1112
|
+
for ISSUE_NUM in $MS_ISSUE_NUMS; do
|
|
1113
|
+
ISSUE_LABELS=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
1114
|
+
--json labels,state \
|
|
1115
|
+
--jq '(.labels | map(.name) | join(",")) + "|" + .state' \
|
|
1116
|
+
2>/dev/null || echo "unknown|unknown")
|
|
1117
|
+
ISSUE_LABEL_STR="${ISSUE_LABELS%%|*}"
|
|
1118
|
+
ISSUE_STATE="${ISSUE_LABELS##*|}"
|
|
1119
|
+
|
|
1120
|
+
if echo "$ISSUE_LABEL_STR" | grep -qE 'workflow:merged|workflow:invalid|needs-human' || [ "$ISSUE_STATE" = "CLOSED" ]; then
|
|
1121
|
+
echo " #$ISSUE_NUM ✓ terminal ($ISSUE_LABEL_STR)"
|
|
1122
|
+
elif echo "$ISSUE_LABEL_STR" | grep -q 'workflow:in-review'; then
|
|
1123
|
+
echo " #$ISSUE_NUM ⟳ stalled at workflow:in-review — resuming via recover-orphans"
|
|
1124
|
+
if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ]; then
|
|
1125
|
+
Skill("recover-orphans", args="--issue $ISSUE_NUM")
|
|
1126
|
+
else
|
|
1127
|
+
echo " WARNING: recover-orphans not installed — cannot auto-resume #$ISSUE_NUM"
|
|
1128
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
1129
|
+
fi
|
|
1130
|
+
else
|
|
1131
|
+
echo " #$ISSUE_NUM ⚠ non-terminal ($ISSUE_LABEL_STR) — will surface in Phase 4 report"
|
|
1132
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
1133
|
+
fi
|
|
1134
|
+
done
|
|
546
1135
|
else
|
|
547
|
-
echo "
|
|
1136
|
+
echo "INFO: Using agent dispatch mode (forgedock CLI not in PATH — run \`npm install -g forgedock\` for engine-mode dispatch)"
|
|
1137
|
+
# Safety Rule 2 (needs-human guarantee): See fast-lane fallback comment above.
|
|
1138
|
+
# /orchestrate enforces the needs-human exclusion at its own phase entry.
|
|
1139
|
+
|
|
1140
|
+
# Snapshot milestone issue list before orchestrate so we can verify afterward.
|
|
1141
|
+
MS_PRE_DISPATCH=$(gh issue list $GH_FLAG \
|
|
1142
|
+
--state open \
|
|
1143
|
+
--limit 200 \
|
|
1144
|
+
--json number,milestone,labels \
|
|
1145
|
+
--jq --arg slug "$MS_SLUG" \
|
|
1146
|
+
'[.[] | select(
|
|
1147
|
+
.milestone != null and
|
|
1148
|
+
(.milestone.title | ascii_downcase | gsub("[^a-z0-9]+"; "-") | ltrimstr("-") | rtrimstr("-")) == $slug and
|
|
1149
|
+
(.labels | map(.name) | any(. == "workflow:merged" or . == "workflow:invalid" or . == "workflow:decomposed" or . == "needs-human") | not)
|
|
1150
|
+
)] | .[].number' \
|
|
1151
|
+
2>/dev/null || echo '')
|
|
1152
|
+
for N in $MS_PRE_DISPATCH; do DISPATCHED_ISSUES+=("$N"); done
|
|
1153
|
+
|
|
548
1154
|
Skill("orchestrate", args="milestone $MS_SLUG")
|
|
1155
|
+
|
|
1156
|
+
# Terminal-state verification after orchestrate fallback.
|
|
1157
|
+
echo "Verifying terminal state for $( echo "$MS_PRE_DISPATCH" | wc -w | tr -d ' ') dispatched milestone issue(s) (orchestrate path)..."
|
|
1158
|
+
for ISSUE_NUM in $MS_PRE_DISPATCH; do
|
|
1159
|
+
ISSUE_LABELS=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
1160
|
+
--json labels,state \
|
|
1161
|
+
--jq '(.labels | map(.name) | join(",")) + "|" + .state' \
|
|
1162
|
+
2>/dev/null || echo "unknown|unknown")
|
|
1163
|
+
ISSUE_LABEL_STR="${ISSUE_LABELS%%|*}"
|
|
1164
|
+
ISSUE_STATE="${ISSUE_LABELS##*|}"
|
|
1165
|
+
|
|
1166
|
+
if echo "$ISSUE_LABEL_STR" | grep -qE 'workflow:merged|workflow:invalid|needs-human' || [ "$ISSUE_STATE" = "CLOSED" ]; then
|
|
1167
|
+
echo " #$ISSUE_NUM ✓ terminal ($ISSUE_LABEL_STR)"
|
|
1168
|
+
else
|
|
1169
|
+
echo " #$ISSUE_NUM ⚠ non-terminal ($ISSUE_LABEL_STR)"
|
|
1170
|
+
STALLED_ISSUES+=("$ISSUE_NUM")
|
|
1171
|
+
fi
|
|
1172
|
+
done
|
|
549
1173
|
fi
|
|
550
1174
|
else
|
|
551
1175
|
if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
|
|
@@ -602,25 +1226,212 @@ echo "$MILESTONES" | jq -c '.[]' | while IFS= read -r milestone; do
|
|
|
602
1226
|
--jq '[.[] | select(.milestone == null and (.labels | map(.name) | any(. == "review-finding")))] | length' \
|
|
603
1227
|
2>/dev/null || echo '0')
|
|
604
1228
|
[ "$MS_FINDINGS" -gt 0 ] && echo "NOTE: $MS_FINDINGS review finding(s) now in fast lane — processed next cycle"
|
|
605
|
-
done
|
|
1229
|
+
done < <(echo "$MILESTONES" | jq -c '.[]')
|
|
606
1230
|
|
|
607
1231
|
echo "Milestone loop complete: $MILESTONE_ITERATIONS milestone(s) processed, $MILESTONE_DEPLOYS milestone ship(s)"
|
|
1232
|
+
|
|
1233
|
+
fi # end SKIP_MILESTONE guard
|
|
1234
|
+
|
|
1235
|
+
# Phase 3 complete — record phase marker
|
|
1236
|
+
PHASE_MARKERS_COMMITTED="${PHASE_MARKERS_COMMITTED:+$PHASE_MARKERS_COMMITTED,}milestone"
|
|
1237
|
+
echo "Phase 3 complete. Committed phases: $PHASE_MARKERS_COMMITTED"
|
|
608
1238
|
```
|
|
609
1239
|
|
|
610
1240
|
---
|
|
611
1241
|
|
|
612
|
-
## Phase 4: Final Report
|
|
1242
|
+
## Phase 4: Final Report and Durable Cycle Annotation
|
|
613
1243
|
|
|
614
|
-
|
|
1244
|
+
Write the machine-readable `FORGE:AUTOPILOT_CYCLE` annotation to the ops issue, then print the human-readable terminal summary. The annotation is written FIRST so that partial-report crashes do not leave a terminal-appearing state without the durable record.
|
|
1245
|
+
|
|
1246
|
+
### 4A: Collect final metrics and write FORGE:AUTOPILOT_CYCLE annotation
|
|
615
1247
|
|
|
616
1248
|
```bash
|
|
617
1249
|
CYCLE_END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1250
|
+
CYCLE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) # best-effort — pipeline start is earlier
|
|
618
1251
|
|
|
619
1252
|
# Re-check final open issue count
|
|
620
1253
|
FINAL_OPEN=$(gh issue list $GH_FLAG --state open --limit 200 --json number --jq 'length' 2>/dev/null || echo '0')
|
|
621
1254
|
FINAL_FAST_LANE=$(gh issue list $GH_FLAG --state open --limit 200 --json number,milestone \
|
|
622
1255
|
--jq '[.[] | select(.milestone == null)] | length' 2>/dev/null || echo '0')
|
|
1256
|
+
FINAL_CI_FAILURES="${RECENT_FAILURES:-0}"
|
|
1257
|
+
FINAL_PENDING_FINDINGS=$(gh issue list $GH_FLAG --state open --label "review-finding" --limit 200 --json number --jq 'length' 2>/dev/null || echo '0')
|
|
1258
|
+
|
|
1259
|
+
# Phase 4 complete — record phase marker (before annotation write so it's in the sentinel block)
|
|
1260
|
+
PHASE_MARKERS_COMMITTED="${PHASE_MARKERS_COMMITTED:+$PHASE_MARKERS_COMMITTED,}report"
|
|
1261
|
+
|
|
1262
|
+
# Build baseline JSON for this cycle (consumed by cycle N+1's Phase 0G)
|
|
1263
|
+
BASELINE_JSON="{\"open_issues\":${FINAL_OPEN},\"ci_failures\":${FINAL_CI_FAILURES},\"pending_findings\":${FINAL_PENDING_FINDINGS}}"
|
|
1264
|
+
|
|
1265
|
+
# Build findings summary (recon-created issue numbers)
|
|
1266
|
+
FINDINGS_SUMMARY="${#RECON_ISSUES[@]:-0} recon issue(s) created"
|
|
1267
|
+
if [ "${#RECON_ISSUES[@]}" -gt 0 ]; then
|
|
1268
|
+
FINDINGS_SUMMARY="$FINDINGS_SUMMARY: ${RECON_ISSUES[*]}"
|
|
1269
|
+
fi
|
|
1270
|
+
|
|
1271
|
+
# Build delta section vs prior cycle
|
|
1272
|
+
DELTA_LINES=""
|
|
1273
|
+
if [ -n "$PREV_CYCLE_ID" ]; then
|
|
1274
|
+
if [ -n "$PREV_CYCLE_OPEN_ISSUES" ]; then
|
|
1275
|
+
DELTA_OPEN=$((FINAL_OPEN - PREV_CYCLE_OPEN_ISSUES))
|
|
1276
|
+
DELTA_SIGN=$( [ "$DELTA_OPEN" -lt 0 ] && echo "" || echo "+")
|
|
1277
|
+
DELTA_LINES="${DELTA_LINES}- Open issues: $PREV_CYCLE_OPEN_ISSUES → $FINAL_OPEN (${DELTA_SIGN}${DELTA_OPEN})\n"
|
|
1278
|
+
fi
|
|
1279
|
+
if [ -n "$PREV_CYCLE_CI_FAILURES" ]; then
|
|
1280
|
+
DELTA_CI=$((FINAL_CI_FAILURES - PREV_CYCLE_CI_FAILURES))
|
|
1281
|
+
DELTA_CI_SIGN=$( [ "$DELTA_CI" -lt 0 ] && echo "" || echo "+")
|
|
1282
|
+
DELTA_LINES="${DELTA_LINES}- CI failures: $PREV_CYCLE_CI_FAILURES → $FINAL_CI_FAILURES (${DELTA_CI_SIGN}${DELTA_CI})\n"
|
|
1283
|
+
fi
|
|
1284
|
+
if [ -n "$PREV_CYCLE_PENDING_FINDINGS" ]; then
|
|
1285
|
+
DELTA_PF=$((FINAL_PENDING_FINDINGS - PREV_CYCLE_PENDING_FINDINGS))
|
|
1286
|
+
DELTA_PF_SIGN=$( [ "$DELTA_PF" -lt 0 ] && echo "" || echo "+")
|
|
1287
|
+
DELTA_LINES="${DELTA_LINES}- Pending findings: $PREV_CYCLE_PENDING_FINDINGS → $FINAL_PENDING_FINDINGS (${DELTA_PF_SIGN}${DELTA_PF})\n"
|
|
1288
|
+
fi
|
|
1289
|
+
DELTA_SECTION="### Deltas vs Cycle ${PREV_CYCLE_ID}\n$(printf '%b' "$DELTA_LINES")"
|
|
1290
|
+
else
|
|
1291
|
+
DELTA_SECTION="### Deltas vs Prior Cycle\nNo prior complete cycle found — this is the baseline cycle."
|
|
1292
|
+
fi
|
|
1293
|
+
|
|
1294
|
+
# Write FORGE:AUTOPILOT_CYCLE annotation to the ops issue (machine-readable durable record)
|
|
1295
|
+
if [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ] && [ "$DRY_RUN" = "false" ]; then
|
|
1296
|
+
gh issue comment "$OPS_ISSUE_NUMBER" $GH_FLAG --body "<!-- FORGE:AUTOPILOT_CYCLE -->
|
|
1297
|
+
## Autopilot Cycle — ${CYCLE_ID}
|
|
1298
|
+
|
|
1299
|
+
**cycle_id**: ${CYCLE_ID}
|
|
1300
|
+
**timestamp**: ${CYCLE_END}
|
|
1301
|
+
**baseline**: ${BASELINE_JSON}
|
|
1302
|
+
**phase_markers**: ${PHASE_MARKERS_COMMITTED}
|
|
1303
|
+
|
|
1304
|
+
$(printf '%b' "$DELTA_SECTION")
|
|
1305
|
+
|
|
1306
|
+
### Findings Created This Cycle
|
|
1307
|
+
${FINDINGS_SUMMARY}
|
|
1308
|
+
|
|
1309
|
+
### Fast Lane
|
|
1310
|
+
- Iterations: ${FAST_LANE_ITERATIONS:-0}
|
|
1311
|
+
- Deploys (staging→${DEFAULT_BRANCH}): ${FAST_LANE_DEPLOYS:-0}
|
|
1312
|
+
- Issues dispatched: ${#DISPATCHED_ISSUES[@]:-0}
|
|
1313
|
+
|
|
1314
|
+
### Milestone Loop
|
|
1315
|
+
- Milestones processed: ${MILESTONE_ITERATIONS:-0}
|
|
1316
|
+
- Milestone ships: ${MILESTONE_DEPLOYS:-0}
|
|
1317
|
+
|
|
1318
|
+
### Autonomy Policy Decisions
|
|
1319
|
+
- Fix mode: ${AUTOPILOT_FIX} (headless: ${AUTOPILOT_HEADLESS})
|
|
1320
|
+
- Approved by policy: ${#POLICY_APPROVED_ISSUES[@]:-0} issue(s)$([ "${#POLICY_APPROVED_ISSUES[@]:-0}" -gt 0 ] && echo " — ${POLICY_APPROVED_ISSUES[*]}" || echo "")
|
|
1321
|
+
- Gated (needs-human): ${#POLICY_GATED_ISSUES[@]:-0} issue(s)$([ "${#POLICY_GATED_ISSUES[@]:-0}" -gt 0 ] && echo " — ${POLICY_GATED_ISSUES[*]}" || echo "")
|
|
1322
|
+
- Deferred (budget ceiling): ${#POLICY_DEFERRED_ISSUES[@]:-0} issue(s)$([ "${#POLICY_DEFERRED_ISSUES[@]:-0}" -gt 0 ] && echo " — ${POLICY_DEFERRED_ISSUES[*]}" || echo "")
|
|
1323
|
+
- Budget used: ${FIX_COUNT:-0} / ${BUDGET_PER_CYCLE_FIXES:-unlimited}
|
|
1324
|
+
- Approve map: {p0:${AUTOPILOT_APPROVE_P0},p1:${AUTOPILOT_APPROVE_P1},p2:${AUTOPILOT_APPROVE_P2},p3:${AUTOPILOT_APPROVE_P3}}
|
|
1325
|
+
|
|
1326
|
+
### Final State
|
|
1327
|
+
- Open issues: ${FINAL_OPEN} total (${FINAL_FAST_LANE} unmilestoned)
|
|
1328
|
+
- CI failures (24h): ${FINAL_CI_FAILURES}
|
|
1329
|
+
- Pending review findings: ${FINAL_PENDING_FINDINGS}
|
|
1330
|
+
|
|
1331
|
+
<!-- FORGE:AUTOPILOT_CYCLE:COMPLETE -->" 2>/dev/null \
|
|
1332
|
+
&& echo "FORGE:AUTOPILOT_CYCLE annotation written to ops issue #$OPS_ISSUE_NUMBER" \
|
|
1333
|
+
|| echo "WARNING: Failed to write FORGE:AUTOPILOT_CYCLE annotation — ops issue #$OPS_ISSUE_NUMBER may be inaccessible"
|
|
1334
|
+
elif [ "$DRY_RUN" = "true" ]; then
|
|
1335
|
+
echo "[DRY-RUN] Would write FORGE:AUTOPILOT_CYCLE annotation to ops issue #$OPS_ISSUE_NUMBER"
|
|
1336
|
+
echo "[DRY-RUN] cycle_id=$CYCLE_ID baseline=$BASELINE_JSON phase_markers=$PHASE_MARKERS_COMMITTED"
|
|
1337
|
+
echo "[DRY-RUN] policy: fix=$AUTOPILOT_FIX headless=$AUTOPILOT_HEADLESS approved=${#POLICY_APPROVED_ISSUES[@]:-0} gated=${#POLICY_GATED_ISSUES[@]:-0} deferred=${#POLICY_DEFERRED_ISSUES[@]:-0} budget_used=$FIX_COUNT/$BUDGET_PER_CYCLE_FIXES"
|
|
1338
|
+
fi
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
### 4B: Recurrence detection
|
|
1342
|
+
|
|
1343
|
+
After writing the current cycle annotation, fetch the last 3 COMPLETE cycle annotations from the ops issue. If the same recon-created issue title prefix appears in ALL 3, create a meta-issue citing the cycle annotations — implementing the recurrence detection claim.
|
|
623
1344
|
|
|
1345
|
+
```bash
|
|
1346
|
+
# Recurrence detection — only run if there are recon findings this cycle and ops issue exists
|
|
1347
|
+
if [ "${#RECON_ISSUES[@]:-0}" -gt 0 ] && [ -n "$OPS_ISSUE_NUMBER" ] && [ "$OPS_ISSUE_NUMBER" != "0" ]; then
|
|
1348
|
+
# Fetch last 3 complete AUTOPILOT_CYCLE annotations (now includes the one we just wrote)
|
|
1349
|
+
LAST_3_CYCLES=$(gh api repos/${GH_REPO}/issues/${OPS_ISSUE_NUMBER}/comments \
|
|
1350
|
+
--jq '[.[] | select(.body | contains("FORGE:AUTOPILOT_CYCLE:COMPLETE"))] | .[-3:] | .[].body' \
|
|
1351
|
+
2>/dev/null || echo '')
|
|
1352
|
+
|
|
1353
|
+
CYCLE_COUNT=$(echo "$LAST_3_CYCLES" | grep -c "FORGE:AUTOPILOT_CYCLE:COMPLETE" || echo '0')
|
|
1354
|
+
|
|
1355
|
+
if [ "${CYCLE_COUNT:-0}" -ge 3 ]; then
|
|
1356
|
+
# Check each recon finding title prefix against all 3 cycle annotations
|
|
1357
|
+
for ISSUE_NUM in "${RECON_ISSUES[@]}"; do
|
|
1358
|
+
FINDING_TITLE=$(gh issue view "$ISSUE_NUM" $GH_FLAG --json title --jq '.title' 2>/dev/null | cut -c1-60 || echo '')
|
|
1359
|
+
[ -z "$FINDING_TITLE" ] && continue
|
|
1360
|
+
|
|
1361
|
+
# Extract the first 30 chars as a pattern (enough to distinguish issues without being too strict)
|
|
1362
|
+
FINDING_PATTERN=$(echo "$FINDING_TITLE" | cut -c1-30)
|
|
1363
|
+
|
|
1364
|
+
# Count how many of the 3 cycles contain this pattern in their findings section
|
|
1365
|
+
MATCH_COUNT=$(echo "$LAST_3_CYCLES" | grep -cF "$FINDING_PATTERN" || echo '0')
|
|
1366
|
+
|
|
1367
|
+
if [ "${MATCH_COUNT:-0}" -ge 3 ]; then
|
|
1368
|
+
echo "RECURRENCE DETECTED: '$FINDING_PATTERN' appeared in all 3 recent cycles — creating meta-issue"
|
|
1369
|
+
|
|
1370
|
+
# Extract cycle IDs from the last 3 annotations for citation
|
|
1371
|
+
CITING_CYCLES=$(echo "$LAST_3_CYCLES" \
|
|
1372
|
+
| grep -oP '(?<=\*\*cycle_id\*\*: )[^\n]+' | tr -d '[:space:]' | tr '\n' ', ' | sed 's/,$//')
|
|
1373
|
+
|
|
1374
|
+
# Dedup check — avoid creating a duplicate meta-issue for the same pattern
|
|
1375
|
+
EXISTING_META=$(gh issue list $GH_FLAG --state open \
|
|
1376
|
+
--search "meta: recurring autopilot finding $FINDING_PATTERN" \
|
|
1377
|
+
--limit 1 --json number --jq '.[0].number // empty' 2>/dev/null || echo '')
|
|
1378
|
+
|
|
1379
|
+
if [ -z "$EXISTING_META" ] && [ "$DRY_RUN" = "false" ]; then
|
|
1380
|
+
gh issue create $GH_FLAG \
|
|
1381
|
+
--title "meta: recurring autopilot finding — $FINDING_PATTERN" \
|
|
1382
|
+
--label "priority:P2" \
|
|
1383
|
+
--body "$(cat <<META_EOF
|
|
1384
|
+
## Problem
|
|
1385
|
+
|
|
1386
|
+
The following finding pattern has appeared in **3 or more consecutive /autopilot cycles**, indicating a systemic issue rather than a one-off occurrence.
|
|
1387
|
+
|
|
1388
|
+
**Pattern**: \`${FINDING_PATTERN}\`
|
|
1389
|
+
**Detected in cycles**: ${CITING_CYCLES}
|
|
1390
|
+
|
|
1391
|
+
This meta-issue is created by /autopilot recurrence detection (Phase 4B) when the same finding pattern appears in 3 consecutive complete cycle annotations on the ops issue (#${OPS_ISSUE_NUMBER}).
|
|
1392
|
+
|
|
1393
|
+
## Root Cause (if known)
|
|
1394
|
+
|
|
1395
|
+
Root cause unknown — the recurrent nature suggests either:
|
|
1396
|
+
1. The underlying issue is not being fixed by the regular work-on pipeline (check if prior issues for this pattern were closed with workflow:invalid or blocked)
|
|
1397
|
+
2. A systemic condition keeps re-triggering the finding (e.g., a flaky CI step, a recurring deployment error)
|
|
1398
|
+
3. The issue is being created and merged but the fix does not hold (regression)
|
|
1399
|
+
|
|
1400
|
+
## Affected Files
|
|
1401
|
+
|
|
1402
|
+
Files to be identified during investigation.
|
|
1403
|
+
|
|
1404
|
+
## Acceptance Criteria
|
|
1405
|
+
|
|
1406
|
+
- [ ] Root cause of recurrence identified
|
|
1407
|
+
- [ ] Fix confirmed: pattern does not appear in 3 subsequent /autopilot cycles after merge
|
|
1408
|
+
- [ ] Prior per-cycle issues for this pattern are closed with explanation
|
|
1409
|
+
|
|
1410
|
+
## Context
|
|
1411
|
+
|
|
1412
|
+
**Ops issue (cycle annotations)**: #${OPS_ISSUE_NUMBER}
|
|
1413
|
+
**Citing cycles**: ${CITING_CYCLES}
|
|
1414
|
+
**Detected by**: /autopilot Phase 4B recurrence detection
|
|
1415
|
+
META_EOF
|
|
1416
|
+
)" \
|
|
1417
|
+
--json number --jq '"Created meta-issue #\(.number) for recurrent pattern: $ENV.FINDING_PATTERN"' \
|
|
1418
|
+
2>/dev/null || echo "WARNING: Failed to create meta-issue for recurrence pattern: $FINDING_PATTERN"
|
|
1419
|
+
elif [ -n "$EXISTING_META" ]; then
|
|
1420
|
+
echo "Recurrence meta-issue already exists (#$EXISTING_META) for pattern '$FINDING_PATTERN' — skipping"
|
|
1421
|
+
elif [ "$DRY_RUN" = "true" ]; then
|
|
1422
|
+
echo "[DRY-RUN] Would create meta-issue for recurrent pattern: $FINDING_PATTERN (cycles: $CITING_CYCLES)"
|
|
1423
|
+
fi
|
|
1424
|
+
fi
|
|
1425
|
+
done
|
|
1426
|
+
else
|
|
1427
|
+
echo "Recurrence check: only $CYCLE_COUNT complete cycle(s) on record — need 3 for detection (will activate after more cycles)"
|
|
1428
|
+
fi
|
|
1429
|
+
fi
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
### 4C: Human-readable terminal summary
|
|
1433
|
+
|
|
1434
|
+
```bash
|
|
624
1435
|
echo ""
|
|
625
1436
|
echo "╔═══════════════════════════════════════════════════╗"
|
|
626
1437
|
echo "║ Autopilot Complete ║"
|
|
@@ -630,6 +1441,9 @@ echo "║ ║"
|
|
|
630
1441
|
cat <<REPORT
|
|
631
1442
|
## Autopilot Cycle Report — $CYCLE_END
|
|
632
1443
|
|
|
1444
|
+
**Cycle ID**: ${CYCLE_ID}
|
|
1445
|
+
**Ops Issue**: #${OPS_ISSUE_NUMBER} (FORGE:AUTOPILOT_CYCLE annotation written)
|
|
1446
|
+
|
|
633
1447
|
### Pipeline State at Start
|
|
634
1448
|
- staging→main PR: ${STAGING_PR_NUMBER:-none}
|
|
635
1449
|
- In-flight issues recovered: $INFLIGHT_ISSUES
|
|
@@ -641,6 +1455,10 @@ cat <<REPORT
|
|
|
641
1455
|
- Recurring failures: ${RECURRING:-none}
|
|
642
1456
|
- Issues created from recon: ${#RECON_ISSUES[@]:-0}
|
|
643
1457
|
|
|
1458
|
+
### Hygiene
|
|
1459
|
+
- Orphaned issues recovered (/recover-orphans): $INFLIGHT_ISSUES
|
|
1460
|
+
- Label hygiene sweep (/cleanup labels): $([ "${CLEANUP_LABELS_FIXED:-0}" -eq 1 ] && echo "ran — see /cleanup output for counts" || echo "skipped (not installed or dry-run)")
|
|
1461
|
+
|
|
644
1462
|
### Fast Lane
|
|
645
1463
|
- Iterations: $FAST_LANE_ITERATIONS
|
|
646
1464
|
- Deploys (staging→$DEFAULT_BRANCH): $FAST_LANE_DEPLOYS
|
|
@@ -650,10 +1468,57 @@ cat <<REPORT
|
|
|
650
1468
|
- Milestones processed: $MILESTONE_ITERATIONS
|
|
651
1469
|
- Milestone ships: $MILESTONE_DEPLOYS
|
|
652
1470
|
|
|
1471
|
+
### Autonomy Policy
|
|
1472
|
+
- Fix mode: $AUTOPILOT_FIX (headless: $AUTOPILOT_HEADLESS)
|
|
1473
|
+
- Approved by policy: ${#POLICY_APPROVED_ISSUES[@]:-0} issue(s)
|
|
1474
|
+
- Gated (needs-human, posted to ops issue): ${#POLICY_GATED_ISSUES[@]:-0} issue(s)
|
|
1475
|
+
- Deferred (budget ceiling hit): ${#POLICY_DEFERRED_ISSUES[@]:-0} issue(s)
|
|
1476
|
+
- Budget used: ${FIX_COUNT:-0} / ${BUDGET_PER_CYCLE_FIXES:-unlimited}
|
|
1477
|
+
|
|
653
1478
|
### Final State
|
|
654
1479
|
- Open issues: $FINAL_OPEN total ($FINAL_FAST_LANE unmilestoned)
|
|
655
1480
|
REPORT
|
|
656
1481
|
|
|
1482
|
+
# Per-issue terminal-state disposition table (GitHub-verified).
|
|
1483
|
+
# Sources DISPATCHED_ISSUES accumulated across all dispatch paths this cycle.
|
|
1484
|
+
# This table is the canonical record of what autopilot dispatched and what state each reached.
|
|
1485
|
+
if [ "${#DISPATCHED_ISSUES[@]}" -gt 0 ]; then
|
|
1486
|
+
echo ""
|
|
1487
|
+
echo "### Dispatch Disposition (GitHub-verified terminal state)"
|
|
1488
|
+
echo ""
|
|
1489
|
+
echo "| Issue | Title (50 chars) | GitHub Label | Terminal? |"
|
|
1490
|
+
echo "|-------|-----------------|-------------|-----------|"
|
|
1491
|
+
DISPATCH_TERMINAL_COUNT=0
|
|
1492
|
+
DISPATCH_STALLED_COUNT=0
|
|
1493
|
+
for ISSUE_NUM in "${DISPATCHED_ISSUES[@]}"; do
|
|
1494
|
+
ISSUE_DATA=$(gh issue view "$ISSUE_NUM" $GH_FLAG \
|
|
1495
|
+
--json number,title,labels,state \
|
|
1496
|
+
--jq '{n: .number, title: (.title | .[0:50]), labels: (.labels | map(.name) | join(",")), state: .state}' \
|
|
1497
|
+
2>/dev/null || echo "{\"n\":$ISSUE_NUM,\"title\":\"(fetch failed)\",\"labels\":\"unknown\",\"state\":\"unknown\"}")
|
|
1498
|
+
DISP_LABELS=$(echo "$ISSUE_DATA" | jq -r '.labels // "unknown"')
|
|
1499
|
+
DISP_STATE=$(echo "$ISSUE_DATA" | jq -r '.state // "unknown"')
|
|
1500
|
+
DISP_TITLE=$(echo "$ISSUE_DATA" | jq -r '.title // "(unknown)"')
|
|
1501
|
+
if echo "$DISP_LABELS" | grep -qE 'workflow:merged|workflow:invalid|needs-human' || [ "$DISP_STATE" = "CLOSED" ]; then
|
|
1502
|
+
echo "| #$ISSUE_NUM | $DISP_TITLE | \`$DISP_LABELS\` | ✓ yes |"
|
|
1503
|
+
DISPATCH_TERMINAL_COUNT=$((DISPATCH_TERMINAL_COUNT + 1))
|
|
1504
|
+
else
|
|
1505
|
+
echo "| #$ISSUE_NUM | $DISP_TITLE | \`$DISP_LABELS\` | ✗ no — stalled |"
|
|
1506
|
+
DISPATCH_STALLED_COUNT=$((DISPATCH_STALLED_COUNT + 1))
|
|
1507
|
+
fi
|
|
1508
|
+
done
|
|
1509
|
+
echo ""
|
|
1510
|
+
echo "**Summary**: ${DISPATCH_TERMINAL_COUNT}/${#DISPATCHED_ISSUES[@]} dispatched issues reached terminal state. ${DISPATCH_STALLED_COUNT} stalled."
|
|
1511
|
+
if [ "$DISPATCH_STALLED_COUNT" -gt 0 ]; then
|
|
1512
|
+
echo ""
|
|
1513
|
+
echo "⚠ Stalled issues re-enter the fast-lane queue on the next /autopilot run."
|
|
1514
|
+
echo " Persistent stalls (3+ cycles): gh issue list $GH_FLAG --label workflow:building"
|
|
1515
|
+
fi
|
|
1516
|
+
else
|
|
1517
|
+
echo ""
|
|
1518
|
+
echo "### Dispatch Disposition"
|
|
1519
|
+
echo "No issues dispatched this cycle."
|
|
1520
|
+
fi
|
|
1521
|
+
|
|
657
1522
|
if [ "$FINAL_FAST_LANE" -eq 0 ]; then
|
|
658
1523
|
echo ""
|
|
659
1524
|
echo "Zero open unmilestoned issues remain. Pipeline is clean."
|
|
@@ -730,15 +1595,31 @@ done
|
|
|
730
1595
|
|
|
731
1596
|
## Safety Rules
|
|
732
1597
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1598
|
+
Rules are listed in **precedence order** — when two rules appear to conflict, the rule with the lower number wins. Rule 0 is inviolable; no other rule can override it.
|
|
1599
|
+
|
|
1600
|
+
**Rule 0 (INVIOLABLE): `--dry-run` means NO side effects.** When `DRY_RUN=true`, autopilot produces a report and exits. No issues are created, no PRs are merged, no labels are changed, no sub-skills with side effects are invoked. This holds regardless of priority (P0, P1, or any other), regardless of loop state, and regardless of what any other rule says. Every dispatch block in this spec checks `DRY_RUN` before acting. If a code path appears to conflict with this rule, `DRY_RUN` wins.
|
|
1601
|
+
|
|
1602
|
+
**Rule 1: `/autopilot` overrides "never merge to main"** — autopilot IS the authorized deploy system. staging→main is normal operation in Phases 2 and 3.
|
|
1603
|
+
|
|
1604
|
+
**Rule 2: Never process `needs-human` issues.** Issues labeled `needs-human` must never be autonomously worked on, regardless of priority. The engine-first dispatch path (Phase 2, Step 1 and Phase 3, Step 1) enforces this with an explicit jq filter. The /orchestrate fallback path delegates this guarantee to /orchestrate and /work-on, which skip `needs-human` issues at their own phase entry. A P0 issue labeled `needs-human` — meaning a human explicitly parked it as requiring human judgment — is reported in the Phase 1C recon output but never added to any dispatch queue. Rule 0 (dry-run) takes precedence over this rule: a dry-run run reports the `needs-human` P0 and takes no action.
|
|
1605
|
+
|
|
1606
|
+
**Rule 3: Never skip investigation** — all issues go through the full `/work-on` pipeline (via /orchestrate or the durable engine) before any fix lands.
|
|
1607
|
+
|
|
1608
|
+
**Rule 4: Never create duplicate issues** — always dedup against existing open issues before creating.
|
|
1609
|
+
|
|
1610
|
+
**Rule 5: Loop safety cap** — max 20 fast-lane iterations prevents infinite loops on stuck state.
|
|
1611
|
+
|
|
1612
|
+
**Rule 6: Autopilot never directly mutates issues or labels.** All issue close and workflow label changes must route through `/cleanup` (for label hygiene) or `/recover-orphans` (for orphan recovery) — commands that verify state before acting and maintain an audit trail. Autopilot composes; it does not own destructive writes. If either command is not installed, log an informational message and continue — no bare `gh issue close` or `gh issue edit --add-label` calls are acceptable in this spec.
|
|
1613
|
+
|
|
1614
|
+
**Rule 6a: Graceful sub-skill degradation** — if /recover-orphans or /cleanup is not installed (extras tier), log an informational message and continue. The pipeline proceeds without hygiene rather than failing.
|
|
1615
|
+
|
|
1616
|
+
**Rule 7: State always re-read** — never trust a cached issue count. Re-query GitHub at each loop iteration.
|
|
1617
|
+
|
|
1618
|
+
**Rule 8: deploy-pr result is authoritative** — if status is not "merged", do not assume the deploy succeeded. Log and continue the loop.
|
|
1619
|
+
|
|
1620
|
+
**Rule 9: Terminal-state verification is mandatory after every dispatch batch.** After every `forgedock run-issue … wait` block or `Skill(orchestrate)` call, autopilot MUST query the actual GitHub label state for each dispatched issue. An issue whose process exits 0 but whose GitHub label is still `workflow:in-review` or `workflow:building` is NOT done — the close phase may have been interrupted. Stalls at `workflow:in-review` with a merged PR are the known recoverable class; autopilot resumes them via `Skill("recover-orphans", args="--issue N")` once before recording them as stalled. The Phase 4 report MUST emit a per-issue disposition table sourced from these GitHub-verified states — self-reported sub-process success is not sufficient. <!-- Added: forge#1751 -->
|
|
1621
|
+
|
|
1622
|
+
**Headless / unattended operation**: `/autopilot` has no human checkpoint and never waits for user input. When invoked via `/loop 4h /autopilot` or any other unattended runner, it runs to completion and exits. Human escalation is exclusively via the `needs-human` label (Rule 2 above) — autopilot surfaces `needs-human` issues in the recon report but never stalls waiting for a response. There is no "Phase 4B confirm before fixing" gate in the current design; that checkpoint was intentionally removed in the #1673 rewrite. If a future design adds a confirmation gate, it must be guarded by both Rule 0 (dry-run) and Rule 2 (needs-human) to remain safe.
|
|
742
1623
|
|
|
743
1624
|
---
|
|
744
1625
|
|
|
@@ -748,4 +1629,4 @@ done
|
|
|
748
1629
|
- **Token budget**: Recon is cheap (mostly API calls). Each orchestrate+deploy cycle is expensive (full /work-on per issue via /orchestrate).
|
|
749
1630
|
- **Idempotent**: Safe to run multiple times — /work-on resumes from pipeline checkpoints, /deploy-pr detects existing open PRs.
|
|
750
1631
|
- **Pairs with /loop**: Run `/loop 4h /autopilot` for continuous improvement cycles.
|
|
751
|
-
- **Event-driven complement**: `/autopilot` is a periodic loop. For targeted signal-driven response (metric regression, incident,
|
|
1632
|
+
- **Event-driven complement**: `/autopilot` is a periodic loop. For targeted signal-driven response (metric regression, incident, CI failure spike), 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.
|