forgedock 1.0.1 → 1.0.2
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 +19 -2
- package/bin/forgedock.mjs +286 -10
- package/commands/analytics.md +129 -52
- package/commands/audit-agents.md +29 -5
- package/commands/audit.md +55 -17
- package/commands/autopilot.md +11 -6
- package/commands/cleanup.md +44 -24
- package/commands/deploy-info.md +7 -3
- package/commands/forge-stats.md +13 -4
- package/commands/forgedock-init.md +534 -0
- package/commands/geo-audit.md +114 -26
- package/commands/incident-response.md +4 -1
- package/commands/issue.md +26 -25
- package/commands/milestone.md +80 -59
- package/commands/orchestrate.md +38 -21
- package/commands/pipeline-health.md +26 -15
- package/commands/qa-sweep.md +68 -5
- package/commands/quality-gate.md +18 -0
- package/commands/review-pr-agents.md +58 -90
- package/commands/review-pr.md +58 -16
- package/commands/rollback.md +8 -5
- package/commands/security-audit.md +68 -12
- package/commands/sync-ecosystem.md +196 -156
- package/commands/work-on/build/architect.md +2 -2
- package/commands/work-on/build/context.md +2 -2
- package/commands/work-on/build/implement.md +3 -3
- package/commands/work-on/build/validate.md +3 -3
- package/commands/work-on/build.md +6 -6
- package/commands/work-on/close.md +59 -30
- package/commands/work-on/decompose.md +6 -6
- package/commands/work-on/investigate.md +16 -20
- package/commands/work-on/review.md +4 -4
- package/commands/work-on-monolithic.md +15 -16
- package/commands/work-on.md +45 -33
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +398 -0
- package/docs/WORKFLOW.md +115 -72
- package/package.json +1 -1
- package/scripts/verify-host-headers.sh +14 -9
- package/scripts/verify-route-registration.sh +10 -10
- package/commands/failure-recon.md +0 -375
package/commands/geo-audit.md
CHANGED
|
@@ -12,14 +12,81 @@ You are the GEO (Generative Engine Optimization) auditor. Pull AI referral data
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Phase 0: Load Config
|
|
16
|
+
|
|
17
|
+
Read `forge.yaml` from the current directory (or the path passed as `$ARGUMENTS`). Gate the entire command on the `services.analytics` section existing.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Verify forge.yaml exists and analytics is configured
|
|
21
|
+
GEO_CONFIG=$(python3 - <<'PYEOF'
|
|
22
|
+
import yaml, sys, os
|
|
23
|
+
|
|
24
|
+
config_path = os.environ.get('FORGE_CONFIG', 'forge.yaml')
|
|
25
|
+
try:
|
|
26
|
+
cfg = yaml.safe_load(open(config_path))
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
print('ERROR: forge.yaml not found. Run `cp forge.yaml.example forge.yaml` and configure your project.')
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
svc = cfg.get('services', {})
|
|
32
|
+
analytics = svc.get('analytics', {})
|
|
33
|
+
if not analytics:
|
|
34
|
+
print('ERROR: forge.yaml missing services.analytics section — geo-audit requires analytics to be configured.')
|
|
35
|
+
print('Add services.analytics.umami and/or services.analytics.clarity to forge.yaml.')
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
paths = cfg.get('paths', {})
|
|
39
|
+
proj = cfg.get('project', {})
|
|
40
|
+
creds = paths.get('credentials', {})
|
|
41
|
+
creds_file = creds.get('file', '') if isinstance(creds, dict) else ''
|
|
42
|
+
|
|
43
|
+
domain = svc.get('domain', '')
|
|
44
|
+
if not domain:
|
|
45
|
+
print('ERROR: forge.yaml missing services.domain — required for page compliance checks.')
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
umami = analytics.get('umami', {})
|
|
49
|
+
clarity = analytics.get('clarity', {})
|
|
50
|
+
owner = proj.get('owner', '')
|
|
51
|
+
repo = proj.get('repo', '')
|
|
52
|
+
|
|
53
|
+
print(domain)
|
|
54
|
+
print('https://' + domain)
|
|
55
|
+
print(umami.get('website_id', ''))
|
|
56
|
+
print(umami.get('url', ''))
|
|
57
|
+
print(clarity.get('project_id', ''))
|
|
58
|
+
print(creds_file)
|
|
59
|
+
print((owner + '/' + repo) if owner and repo else '')
|
|
60
|
+
PYEOF
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if echo "$GEO_CONFIG" | grep -q '^ERROR:'; then
|
|
64
|
+
echo "$GEO_CONFIG"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Assign config variables
|
|
69
|
+
DOMAIN=$(echo "$GEO_CONFIG" | sed -n '1p')
|
|
70
|
+
SITE_URL=$(echo "$GEO_CONFIG" | sed -n '2p')
|
|
71
|
+
UMAMI_WEBSITE_ID=$(echo "$GEO_CONFIG" | sed -n '3p')
|
|
72
|
+
UMAMI_API=$(echo "$GEO_CONFIG" | sed -n '4p')
|
|
73
|
+
CLARITY_PROJECT=$(echo "$GEO_CONFIG" | sed -n '5p')
|
|
74
|
+
CREDENTIALS_FILE=$(echo "$GEO_CONFIG" | sed -n '6p')
|
|
75
|
+
GH_REPO=$(echo "$GEO_CONFIG" | sed -n '7p')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
15
80
|
## Constants
|
|
16
81
|
|
|
17
82
|
```
|
|
18
|
-
DOMAIN
|
|
19
|
-
SITE_URL
|
|
20
|
-
UMAMI_WEBSITE_ID =
|
|
21
|
-
UMAMI_API
|
|
22
|
-
CLARITY_PROJECT
|
|
83
|
+
DOMAIN = (from forge.yaml → services.domain)
|
|
84
|
+
SITE_URL = (from forge.yaml → "https://" + services.domain)
|
|
85
|
+
UMAMI_WEBSITE_ID = (from forge.yaml → services.analytics.umami.website_id)
|
|
86
|
+
UMAMI_API = (from forge.yaml → services.analytics.umami.url)
|
|
87
|
+
CLARITY_PROJECT = (from forge.yaml → services.analytics.clarity.project_id)
|
|
88
|
+
CREDENTIALS_FILE = (from forge.yaml → paths.credentials.file)
|
|
89
|
+
GH_REPO = (from forge.yaml → project.owner + "/" + project.repo)
|
|
23
90
|
TODAY = (current date in YYYY-MM-DD)
|
|
24
91
|
THIRTY_DAYS_AGO = (TODAY minus 30 days)
|
|
25
92
|
|
|
@@ -41,11 +108,11 @@ AI_REFERRERS = [
|
|
|
41
108
|
|
|
42
109
|
### 1A. Umami — AI Referrer Metrics
|
|
43
110
|
|
|
44
|
-
Authenticate with Umami (credentials from
|
|
111
|
+
Authenticate with Umami (credentials from credentials file set in forge.yaml):
|
|
45
112
|
```bash
|
|
46
|
-
UMAMI_USER=$(python3 -c "import yaml; print(yaml.safe_load(open('
|
|
47
|
-
UMAMI_PASS=$(python3 -c "import yaml; print(yaml.safe_load(open('
|
|
48
|
-
UMAMI_TOKEN=$(curl -s -X POST "
|
|
113
|
+
UMAMI_USER=$(python3 -c "import yaml; print(yaml.safe_load(open('$CREDENTIALS_FILE'))['umami']['username'])")
|
|
114
|
+
UMAMI_PASS=$(python3 -c "import yaml; print(yaml.safe_load(open('$CREDENTIALS_FILE'))['umami']['password'])")
|
|
115
|
+
UMAMI_TOKEN=$(curl -s -X POST "$UMAMI_API/api/auth/login" \
|
|
49
116
|
-H "Content-Type: application/json" \
|
|
50
117
|
-d "{\"username\":\"$UMAMI_USER\",\"password\":\"$UMAMI_PASS\"}" | jq -r '.token')
|
|
51
118
|
```
|
|
@@ -54,7 +121,7 @@ Then pull referrer metrics for the last 30 days:
|
|
|
54
121
|
```bash
|
|
55
122
|
START_AT=$(date -d '30 days ago' +%s)000
|
|
56
123
|
END_AT=$(date +%s)000
|
|
57
|
-
curl -s "
|
|
124
|
+
curl -s "$UMAMI_API/api/websites/$UMAMI_WEBSITE_ID/metrics?startAt=${START_AT}&endAt=${END_AT}&type=referrer" \
|
|
58
125
|
-H "Authorization: Bearer $UMAMI_TOKEN" | jq '.'
|
|
59
126
|
```
|
|
60
127
|
|
|
@@ -62,7 +129,7 @@ From the results, filter for AI_REFERRERS. Record each AI referrer's session cou
|
|
|
62
129
|
|
|
63
130
|
Also pull landing page metrics to see WHERE AI traffic lands:
|
|
64
131
|
```bash
|
|
65
|
-
curl -s "
|
|
132
|
+
curl -s "$UMAMI_API/api/websites/$UMAMI_WEBSITE_ID/metrics?startAt=${START_AT}&endAt=${END_AT}&type=url" \
|
|
66
133
|
-H "Authorization: Bearer $UMAMI_TOKEN" | jq '.'
|
|
67
134
|
```
|
|
68
135
|
|
|
@@ -70,7 +137,7 @@ For previous period comparison (30-60 days ago):
|
|
|
70
137
|
```bash
|
|
71
138
|
PREV_START=$(date -d '60 days ago' +%s)000
|
|
72
139
|
PREV_END=$(date -d '30 days ago' +%s)000
|
|
73
|
-
curl -s "
|
|
140
|
+
curl -s "$UMAMI_API/api/websites/$UMAMI_WEBSITE_ID/metrics?startAt=${PREV_START}&endAt=${PREV_END}&type=referrer" \
|
|
74
141
|
-H "Authorization: Bearer $UMAMI_TOKEN" | jq '.'
|
|
75
142
|
```
|
|
76
143
|
|
|
@@ -96,17 +163,17 @@ Cross-reference Clarity referrer data with Umami to validate numbers.
|
|
|
96
163
|
|
|
97
164
|
Get the sitemap to find all public pages:
|
|
98
165
|
```bash
|
|
99
|
-
curl -s "
|
|
166
|
+
curl -s "$SITE_URL/sitemap.xml" | grep -oP '<loc>\K[^<]+' | sort
|
|
100
167
|
```
|
|
101
168
|
|
|
102
169
|
Also check llms.txt for listed pages:
|
|
103
170
|
```bash
|
|
104
|
-
curl -s "
|
|
171
|
+
curl -s "$SITE_URL/llms.txt"
|
|
105
172
|
```
|
|
106
173
|
|
|
107
174
|
And the full version:
|
|
108
175
|
```bash
|
|
109
|
-
curl -s "
|
|
176
|
+
curl -s "$SITE_URL/llms-full.txt"
|
|
110
177
|
```
|
|
111
178
|
|
|
112
179
|
Build the list of all unique public pages from both sources.
|
|
@@ -133,7 +200,7 @@ curl -s "PAGE_URL" | grep -oP '<script type="application/ld\+json">\K[^<]+' | jq
|
|
|
133
200
|
```bash
|
|
134
201
|
curl -s "PAGE_URL" | grep -oP 'property="og:title" content="\K[^"]+'
|
|
135
202
|
```
|
|
136
|
-
- PASS: og:title exists and is NOT the generic site default (e.g., not just
|
|
203
|
+
- PASS: og:title exists and is NOT the generic site default (e.g., not just the project name or empty)
|
|
137
204
|
- FAIL: missing or generic → flag "Missing/generic OG tags"
|
|
138
205
|
|
|
139
206
|
Also check og:description:
|
|
@@ -152,7 +219,7 @@ Check if page URL appears in llms.txt content.
|
|
|
152
219
|
### 2C. Check robots.txt for AI Bots
|
|
153
220
|
|
|
154
221
|
```bash
|
|
155
|
-
curl -s "
|
|
222
|
+
curl -s "$SITE_URL/robots.txt"
|
|
156
223
|
```
|
|
157
224
|
|
|
158
225
|
Known AI crawler user-agents to check for:
|
|
@@ -297,16 +364,35 @@ gh issue edit {NUMBER} --milestone "geo-ai-discoverability"
|
|
|
297
364
|
|
|
298
365
|
### Add GEO issues to Project board
|
|
299
366
|
|
|
300
|
-
|
|
367
|
+
Read project board IDs from `forge.yaml → project_board`. If `project_board` is not configured, skip board integration.
|
|
301
368
|
|
|
302
369
|
```bash
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
370
|
+
# Read project board config from forge.yaml
|
|
371
|
+
BOARD_CONFIG=$(python3 - <<'PYEOF'
|
|
372
|
+
import yaml, sys
|
|
373
|
+
cfg = yaml.safe_load(open('forge.yaml'))
|
|
374
|
+
board = cfg.get('project_board', {})
|
|
375
|
+
if not board:
|
|
376
|
+
print('')
|
|
377
|
+
sys.exit(0)
|
|
378
|
+
print(board.get('owner', cfg.get('project', {}).get('owner', '')))
|
|
379
|
+
print(str(board.get('project_number', '')))
|
|
380
|
+
print(board.get('project_id', ''))
|
|
381
|
+
PYEOF
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
BOARD_OWNER=$(echo "$BOARD_CONFIG" | sed -n '1p')
|
|
385
|
+
BOARD_NUMBER=$(echo "$BOARD_CONFIG" | sed -n '2p')
|
|
386
|
+
BOARD_ID=$(echo "$BOARD_CONFIG" | sed -n '3p')
|
|
387
|
+
|
|
388
|
+
if [ -n "$BOARD_OWNER" ] && [ -n "$BOARD_NUMBER" ]; then
|
|
389
|
+
ISSUE_URL="https://github.com/$GH_REPO/issues/${ISSUE_NUM}"
|
|
390
|
+
ITEM_ID=$(gh project item-add "$BOARD_NUMBER" --owner "$BOARD_OWNER" --url "$ISSUE_URL" --format json --jq '.id' 2>/dev/null)
|
|
391
|
+
if [ -n "$ITEM_ID" ] && [ -n "$BOARD_ID" ]; then
|
|
392
|
+
# Set Status, Lane, Priority using field IDs from forge.yaml → project_board.field_ids
|
|
393
|
+
# See docs/CONFIG.md → project_board section for how to get field IDs
|
|
394
|
+
echo "Issue added to project board. Set Status/Lane/Priority manually or configure field_ids in forge.yaml."
|
|
395
|
+
fi
|
|
310
396
|
fi
|
|
311
397
|
```
|
|
312
398
|
|
|
@@ -336,8 +422,10 @@ fi
|
|
|
336
422
|
|
|
337
423
|
## Error Handling
|
|
338
424
|
|
|
425
|
+
- **forge.yaml missing**: Print setup instructions and exit. Do not proceed without config.
|
|
426
|
+
- **services.analytics missing**: Print configuration instructions and exit.
|
|
339
427
|
- **Umami auth fails**: Try SOPS credentials. If still broken, skip Umami and note in report.
|
|
340
428
|
- **Clarity MCP fails**: Skip Clarity data, proceed with Umami only.
|
|
341
|
-
- **curl to
|
|
429
|
+
- **curl to site fails**: Report "Site unreachable — cannot run compliance checks" and only show referral data.
|
|
342
430
|
- **No AI referral data at all**: Report "No AI referral traffic detected in the last 30 days" — this IS an insight (we need to improve discoverability).
|
|
343
431
|
- **Rate limits or timeouts**: Note which checks were incomplete. Partial audit > no audit.
|
|
@@ -7,6 +7,9 @@ argument-hint: [issue number | "active" | "postmortem {issue}"]
|
|
|
7
7
|
|
|
8
8
|
**Input**: $ARGUMENTS
|
|
9
9
|
|
|
10
|
+
**Config variables used by this command** (set in `forge.yaml`):
|
|
11
|
+
- `{REPO_PATH}` ← `paths.root` — project repository root
|
|
12
|
+
|
|
10
13
|
You are the pipeline's incident response coordinator. When production goes down or a critical bug surfaces, this command orchestrates the response: validates the hotfix before deploy, reconstructs the incident timeline, and produces a post-incident analysis.
|
|
11
14
|
|
|
12
15
|
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`. User can override with `--model <name>`.
|
|
@@ -106,7 +109,7 @@ You are validating a hotfix for a P0 production incident.
|
|
|
106
109
|
|
|
107
110
|
Issue: #{NUMBER} — {TITLE}
|
|
108
111
|
Fix branch: {FIX_BRANCH}
|
|
109
|
-
Repo:
|
|
112
|
+
Repo: {REPO_PATH}
|
|
110
113
|
|
|
111
114
|
Your job:
|
|
112
115
|
1. Read the fix diff: git diff origin/main..origin/{FIX_BRANCH}
|
package/commands/issue.md
CHANGED
|
@@ -35,12 +35,20 @@ The user's input (`$ARGUMENTS`) can be:
|
|
|
35
35
|
|
|
36
36
|
### 1A: Resolve target repository
|
|
37
37
|
|
|
38
|
+
Read `forge.yaml` to build the routing table dynamically:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
|
|
42
|
+
GH_FLAG="-R $GH_REPO"
|
|
43
|
+
REPO_PATH=$(yq '.paths.root' forge.yaml)
|
|
44
|
+
STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
|
|
45
|
+
# Satellite repos from forge.yaml → repos.satellites (each has: prefix, repo, staging_branch)
|
|
46
|
+
```
|
|
47
|
+
|
|
38
48
|
| Prefix | Repository | GH_REPO | GH_FLAG |
|
|
39
49
|
|--------|-----------|---------|---------|
|
|
40
|
-
| (none)
|
|
41
|
-
| `
|
|
42
|
-
| `mcp:` | MCP Server | `RapierCraft/alterlab-mcp-server` | `-R RapierCraft/alterlab-mcp-server` |
|
|
43
|
-
| `n8n:` | n8n Node | `RapierCraft/n8n-nodes-alterlab` | `-R RapierCraft/n8n-nodes-alterlab` |
|
|
50
|
+
| (none) | Default project | `{GH_REPO}` from `forge.yaml → project.owner/repo` | (none / use `{GH_FLAG}`) |
|
|
51
|
+
| `{satellite_prefix}:` | Satellite repo | `{satellite.repo}` from `forge.yaml → repos.satellites` | `-R {satellite.repo}` |
|
|
44
52
|
|
|
45
53
|
### 1B: Classify issue type
|
|
46
54
|
|
|
@@ -88,21 +96,10 @@ Pick ONE primary category label:
|
|
|
88
96
|
|
|
89
97
|
### 2A: Identify the domain
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
| Domain | Key Entry Points |
|
|
94
|
-
|--------|-----------------|
|
|
95
|
-
| BILLING | `routers/billing.py`, `core/pricing.py`, `services/credit_service.py` |
|
|
96
|
-
| SCRAPING | `unified_consumer.py`, `queues.py`, `domain_playbooks.json` |
|
|
97
|
-
| AUTH | `core/auth.py`, `routers/auth.py`, `dependencies.py` |
|
|
98
|
-
| DATABASE | `infra/migrations/`, `models/`, `db/` |
|
|
99
|
-
| FRONTEND | `web/src/app/`, `web/src/components/`, `web/src/lib/` |
|
|
100
|
-
| CORTEX | `cortex_client.py`, `routers/cortex.py` |
|
|
101
|
-
| INFRA | `.github/workflows/`, `docker-compose.yml`, `docker-compose.prod.yml`, `infra/traefik/` |
|
|
99
|
+
Map the issue to its domain and key files. Domain structure comes from the project — check `forge.yaml → review.context` and the project's `CLAUDE.md` for project-specific domain maps.
|
|
102
100
|
|
|
103
|
-
For
|
|
104
|
-
For
|
|
105
|
-
For n8n issues: read the repo's `nodes/` and `credentials/` directories
|
|
101
|
+
For ForgeDock issues (this repo): `commands/*.md`, `CLAUDE.md`, `install.sh`, `bin/`
|
|
102
|
+
For satellite repo issues: read the satellite repo's source directory (from `forge.yaml → repos.satellites`)
|
|
106
103
|
|
|
107
104
|
### 2B: Read the affected files
|
|
108
105
|
|
|
@@ -113,12 +110,12 @@ For n8n issues: read the repo's `nodes/` and `credentials/` directories
|
|
|
113
110
|
3. **Import chains**: If the issue is about a function, check what imports it and what it imports.
|
|
114
111
|
|
|
115
112
|
```bash
|
|
116
|
-
#
|
|
117
|
-
# Check staging first (most recent deployable state), then
|
|
118
|
-
git fetch origin
|
|
113
|
+
# Resolve the right branch to read from
|
|
114
|
+
# Check the staging branch first (most recent deployable state), then the default branch
|
|
115
|
+
git fetch origin {STAGING_BRANCH} {DEFAULT_BRANCH} 2>/dev/null
|
|
119
116
|
|
|
120
117
|
# Read the files relevant to the issue
|
|
121
|
-
# Use git show origin/
|
|
118
|
+
# Use git show origin/{STAGING_BRANCH}:{filepath} for project files
|
|
122
119
|
```
|
|
123
120
|
|
|
124
121
|
### 2C: Identify ALL affected files
|
|
@@ -423,11 +420,15 @@ else
|
|
|
423
420
|
fi
|
|
424
421
|
```
|
|
425
422
|
|
|
426
|
-
### 4D: Add to Project board (
|
|
423
|
+
### 4D: Add to Project board (if configured)
|
|
427
424
|
|
|
428
425
|
```bash
|
|
429
|
-
#
|
|
430
|
-
|
|
426
|
+
# Add to project board if forge.yaml → project_board is configured
|
|
427
|
+
PROJECT_NUMBER=$(yq '.project_board.project_number // ""' forge.yaml)
|
|
428
|
+
PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
|
|
429
|
+
if [ -n "$PROJECT_NUMBER" ] && [ "$PROJECT_NUMBER" != "null" ]; then
|
|
430
|
+
gh project item-add "$PROJECT_NUMBER" --owner "$PROJECT_BOARD_OWNER" --url "${ISSUE_URL}" 2>/dev/null || true
|
|
431
|
+
fi
|
|
431
432
|
```
|
|
432
433
|
|
|
433
434
|
### 4E: Report to user
|
package/commands/milestone.md
CHANGED
|
@@ -13,28 +13,51 @@ Milestones are the top-level planning unit. They group related issues into a shi
|
|
|
13
13
|
|
|
14
14
|
**You have access to ALL tools** — Task tool, Skill tool, sub-agents, everything.
|
|
15
15
|
|
|
16
|
+
## Config Resolution
|
|
17
|
+
|
|
18
|
+
Read `forge.yaml` at the project root to resolve all project-specific variables before running any commands:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Parse forge.yaml for project context
|
|
22
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' forge.yaml)
|
|
23
|
+
GH_FLAG="-R $GH_REPO"
|
|
24
|
+
REPO_PATH=$(yq '.paths.root' forge.yaml)
|
|
25
|
+
PROJECT_NAME=$(yq '.project.name' forge.yaml)
|
|
26
|
+
STAGING_BRANCH=$(yq '.branches.staging' forge.yaml)
|
|
27
|
+
PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' forge.yaml)
|
|
28
|
+
PROJECT_NUMBER=$(yq '.project_board.project_number // "1"' forge.yaml)
|
|
29
|
+
PROJECT_ID=$(yq '.project_board.project_id' forge.yaml)
|
|
30
|
+
# Build satellite repo map from repos.satellites list
|
|
31
|
+
# Each satellite: { prefix, repo, staging_branch }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{PROJECT_NAME}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD_OWNER}`, `{PROJECT_NUMBER}`, and `{PROJECT_ID}` references below are populated from `forge.yaml`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
16
38
|
## Multi-Repo Support
|
|
17
39
|
|
|
18
|
-
Milestones can span
|
|
40
|
+
Milestones can span multiple repositories. Project context (repo map, paths, conventions) comes from `forge.yaml`.
|
|
19
41
|
|
|
20
42
|
### Cross-Repo Milestones
|
|
21
43
|
|
|
22
|
-
Some milestones
|
|
44
|
+
Some milestones require changes across multiple repos — the platform, SDKs, satellite services, etc. For these:
|
|
23
45
|
|
|
24
|
-
- **The milestone is created in the primary repo** (
|
|
25
|
-
- **Issues are created in the repo where the code lives** —
|
|
46
|
+
- **The milestone is created in the primary repo** (`{GH_REPO}` — the default repo from `forge.yaml`)
|
|
47
|
+
- **Issues are created in the repo where the code lives** — satellite repos come from `forge.yaml → repos.satellites`
|
|
26
48
|
- **Each repo gets its own milestone branch** (`milestone/{slug}`) if it has issues in the milestone
|
|
27
|
-
- **Issues reference the parent milestone** in their body: `Part of cross-repo milestone: **{TITLE}** (primary:
|
|
49
|
+
- **Issues reference the parent milestone** in their body: `Part of cross-repo milestone: **{TITLE}** (primary: {GH_REPO})`
|
|
28
50
|
|
|
29
51
|
### Repo Prefixes (same as `/work-on`)
|
|
30
52
|
|
|
53
|
+
Prefixes and repos are defined in `forge.yaml → repos.satellites`. Read the config to build the routing table:
|
|
54
|
+
|
|
31
55
|
| Prefix | Repo |
|
|
32
56
|
|--------|------|
|
|
33
|
-
| _(none)_ | `
|
|
34
|
-
| `
|
|
35
|
-
| `n8n:` | `RapierCraft/n8n-nodes-alterlab` |
|
|
57
|
+
| _(none)_ | `{GH_REPO}` (default) |
|
|
58
|
+
| `{SATELLITE_PREFIX}:` | `{SATELLITE_REPO}` (from `forge.yaml → repos.satellites`) |
|
|
36
59
|
|
|
37
|
-
When decomposing a milestone, tag each proposed issue with its target repo. Use `-R` flag for non-default repos.
|
|
60
|
+
When decomposing a milestone, tag each proposed issue with its target repo. Use `-R {SATELLITE_REPO}` flag for non-default repos.
|
|
38
61
|
|
|
39
62
|
---
|
|
40
63
|
|
|
@@ -76,7 +99,7 @@ Capture the milestone number from the response.
|
|
|
76
99
|
### Step 3: Create the milestone branch
|
|
77
100
|
|
|
78
101
|
```bash
|
|
79
|
-
cd {REPO_PATH}
|
|
102
|
+
cd {REPO_PATH}
|
|
80
103
|
git fetch origin main
|
|
81
104
|
git branch milestone/{slug} origin/main
|
|
82
105
|
git push origin milestone/{slug}
|
|
@@ -86,7 +109,7 @@ git push origin milestone/{slug}
|
|
|
86
109
|
|
|
87
110
|
Use a Task sub-agent to analyze the milestone scope and propose issues:
|
|
88
111
|
|
|
89
|
-
> You are planning the scope for milestone "{TITLE}" on the
|
|
112
|
+
> You are planning the scope for milestone "{TITLE}" on the {PROJECT_NAME} platform.
|
|
90
113
|
>
|
|
91
114
|
> **Milestone description**: {DESCRIPTION}
|
|
92
115
|
>
|
|
@@ -95,19 +118,16 @@ Use a Task sub-agent to analyze the milestone scope and propose issues:
|
|
|
95
118
|
> - Specific enough for the `/work-on` pipeline to pick up and implement
|
|
96
119
|
> - Ordered by dependencies (if B needs A's API, A comes first)
|
|
97
120
|
>
|
|
98
|
-
> **
|
|
99
|
-
> - **
|
|
100
|
-
> - **
|
|
101
|
-
> - **
|
|
102
|
-
> - **MCP Server** (alterlab-mcp-server/ — SEPARATE REPO): MCP tools for AI assistants, TypeScript, @modelcontextprotocol/sdk
|
|
103
|
-
> - **n8n Node** (n8n-nodes-alterlab/ — SEPARATE REPO): n8n community node, TypeScript
|
|
104
|
-
> - **Chrome Extension** (tools/chrome-extension/): Cookie capture for BYOS
|
|
121
|
+
> **Project ecosystem context** (from `forge.yaml`):
|
|
122
|
+
> - **Primary repo** (`{GH_REPO}`): The main project codebase at `{REPO_PATH}`
|
|
123
|
+
> - **Tech stack**: {review.tech_stack from forge.yaml, or read CLAUDE.md for project context}
|
|
124
|
+
> - **Satellite repos**: Read from `forge.yaml → repos.satellites` (each has a prefix and repo)
|
|
105
125
|
>
|
|
106
|
-
> Check the existing codebase to understand current architecture before proposing changes.
|
|
126
|
+
> Check the existing codebase at `{REPO_PATH}` to understand current architecture before proposing changes.
|
|
107
127
|
>
|
|
108
128
|
> **Output format**: Numbered list of proposed issues, each with:
|
|
109
|
-
> - **Title**: Actionable, specific
|
|
110
|
-
> - **Repo**: Which repo this issue belongs to (
|
|
129
|
+
> - **Title**: Actionable, specific
|
|
130
|
+
> - **Repo**: Which repo this issue belongs to (default or satellite prefix from forge.yaml)
|
|
111
131
|
> - **Type**: `feature`, `bug`, `refactor`, `infra`
|
|
112
132
|
> - **Priority**: P1-P3
|
|
113
133
|
> - **Size**: S (1-2 files), M (3-5 files), L (6+ files)
|
|
@@ -126,8 +146,8 @@ Present the proposed issues to the user. Ask:
|
|
|
126
146
|
For each approved issue, create it in the **correct repo** based on the scope analysis:
|
|
127
147
|
|
|
128
148
|
```bash
|
|
129
|
-
# For
|
|
130
|
-
gh issue create \
|
|
149
|
+
# For default repo issues:
|
|
150
|
+
gh issue create {GH_FLAG} \
|
|
131
151
|
--title "{fix|feat|refactor}: {issue_title}" \
|
|
132
152
|
--label "{type},{priority}" \
|
|
133
153
|
--milestone "{TITLE}" \
|
|
@@ -190,7 +210,7 @@ Files that need changes:
|
|
|
190
210
|
|
|
191
211
|
## Context
|
|
192
212
|
|
|
193
|
-
Part of cross-repo milestone: **{MILESTONE_TITLE}** (primary:
|
|
213
|
+
Part of cross-repo milestone: **{MILESTONE_TITLE}** (primary: {GH_REPO})
|
|
194
214
|
**Type**: {feature|bug|refactor|infra}
|
|
195
215
|
|
|
196
216
|
## Dependencies
|
|
@@ -202,17 +222,17 @@ BODY_EOF
|
|
|
202
222
|
|
|
203
223
|
### Step 6B: Add issues to Project board
|
|
204
224
|
|
|
205
|
-
For each created issue, add it to the GitHub Project board with initial fields. Reference
|
|
225
|
+
For each created issue, add it to the GitHub Project board with initial fields. Reference `forge.yaml → project_board` for field IDs, or `docs/CONFIG.md` → "Project Board" for lookup commands.
|
|
206
226
|
|
|
207
227
|
```bash
|
|
208
228
|
for ISSUE_NUM in {created_issue_numbers}; do
|
|
209
229
|
ISSUE_URL="https://github.com/{GH_REPO}/issues/${ISSUE_NUM}"
|
|
210
|
-
ITEM_ID=$(gh project item-add
|
|
230
|
+
ITEM_ID=$(gh project item-add {PROJECT_NUMBER} --owner {PROJECT_BOARD_OWNER} --url "$ISSUE_URL" --format json --jq '.id' 2>/dev/null)
|
|
211
231
|
if [ -n "$ITEM_ID" ]; then
|
|
212
|
-
gh project item-edit --project-id
|
|
213
|
-
gh project item-edit --project-id
|
|
214
|
-
gh project item-edit --project-id
|
|
215
|
-
gh project item-edit --project-id
|
|
232
|
+
gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_STATUS_FIELD_ID} --single-select-option-id {PROJECT_BOARD_STATUS_TODO_ID} 2>/dev/null || true # Status=Todo
|
|
233
|
+
gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_LANE_FIELD_ID} --single-select-option-id {PROJECT_BOARD_LANE_FEATURE_ID} 2>/dev/null || true # Lane=Feature
|
|
234
|
+
gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_COMPONENT_FIELD_ID} --single-select-option-id {COMPONENT_OPTION_ID} 2>/dev/null || true # Component (from forge.yaml → project_board.fields)
|
|
235
|
+
gh project item-edit --project-id {PROJECT_ID} --id "$ITEM_ID" --field-id {PROJECT_BOARD_PRIORITY_FIELD_ID} --single-select-option-id {PRIORITY_OPTION_ID} 2>/dev/null || true # Priority (from label)
|
|
216
236
|
fi
|
|
217
237
|
done
|
|
218
238
|
```
|
|
@@ -308,13 +328,13 @@ Wait for confirmation.
|
|
|
308
328
|
Before shipping, ensure the milestone branch is up to date with `staging` (picks up any fast-lane fixes that accumulated since the milestone branch was created):
|
|
309
329
|
|
|
310
330
|
```bash
|
|
311
|
-
cd {REPO_PATH}
|
|
312
|
-
git fetch origin
|
|
331
|
+
cd {REPO_PATH}
|
|
332
|
+
git fetch origin {STAGING_BRANCH} milestone/{slug}
|
|
313
333
|
git checkout milestone/{slug}
|
|
314
|
-
git merge origin/
|
|
334
|
+
git merge origin/{STAGING_BRANCH} --no-edit
|
|
315
335
|
# If conflicts, report to user and STOP — do not auto-resolve
|
|
316
336
|
git push origin milestone/{slug}
|
|
317
|
-
git checkout
|
|
337
|
+
git checkout {STAGING_BRANCH}
|
|
318
338
|
```
|
|
319
339
|
|
|
320
340
|
### Step 2.5: Pre-Merge Hunk-Loss Audit (MANDATORY before creating PR)
|
|
@@ -325,12 +345,12 @@ This step identifies "at-risk hunks" — staging-only content in files the miles
|
|
|
325
345
|
|
|
326
346
|
```bash
|
|
327
347
|
cd {REPO_PATH}
|
|
328
|
-
git fetch origin
|
|
348
|
+
git fetch origin {STAGING_BRANCH} milestone/{slug}
|
|
329
349
|
|
|
330
350
|
echo "=== Pre-Merge Hunk-Loss Audit ==="
|
|
331
351
|
|
|
332
|
-
# Files the milestone branch changes vs staging
|
|
333
|
-
MILESTONE_FILES=$(git diff --name-only origin/
|
|
352
|
+
# Files the milestone branch changes vs the staging branch
|
|
353
|
+
MILESTONE_FILES=$(git diff --name-only origin/{STAGING_BRANCH}...origin/milestone/{slug})
|
|
334
354
|
|
|
335
355
|
if [ -z "$MILESTONE_FILES" ]; then
|
|
336
356
|
echo "No files changed by milestone vs staging — nothing to audit. Proceeding to PR creation."
|
|
@@ -339,8 +359,8 @@ else
|
|
|
339
359
|
# NOTE: $MILESTONE_FILES is intentionally unquoted here — word-splitting is required to pass
|
|
340
360
|
# each filename as a separate path argument to git diff. Quoting would collapse the newline-
|
|
341
361
|
# separated list into a single argument matching no real path, silently producing an empty diff.
|
|
342
|
-
# Filenames with spaces are not
|
|
343
|
-
AT_RISK=$(git diff origin/milestone/{slug}...origin/
|
|
362
|
+
# Filenames with spaces are not expected in standard project paths.
|
|
363
|
+
AT_RISK=$(git diff origin/milestone/{slug}...origin/{STAGING_BRANCH} -- $MILESTONE_FILES 2>/dev/null)
|
|
344
364
|
AT_RISK_COUNT=$(echo "$AT_RISK" | grep -c '^@@' 2>/dev/null || echo 0)
|
|
345
365
|
|
|
346
366
|
if [ "$AT_RISK_COUNT" -eq 0 ]; then
|
|
@@ -350,9 +370,9 @@ else
|
|
|
350
370
|
echo "Affected files:"
|
|
351
371
|
echo "$AT_RISK" | grep '^--- a/' | sed 's|^--- a/||'
|
|
352
372
|
|
|
353
|
-
echo "Rebasing milestone/{slug} onto origin/
|
|
373
|
+
echo "Rebasing milestone/{slug} onto origin/{STAGING_BRANCH} to absorb at-risk hunks..."
|
|
354
374
|
git checkout milestone/{slug}
|
|
355
|
-
git rebase origin/
|
|
375
|
+
git rebase origin/{STAGING_BRANCH}
|
|
356
376
|
|
|
357
377
|
REBASE_EXIT=$?
|
|
358
378
|
if [ "$REBASE_EXIT" -ne 0 ]; then
|
|
@@ -362,21 +382,21 @@ else
|
|
|
362
382
|
|
|
363
383
|
# Abort the failed rebase and return to a clean state
|
|
364
384
|
git rebase --abort 2>/dev/null || true
|
|
365
|
-
git checkout
|
|
385
|
+
git checkout {STAGING_BRANCH}
|
|
366
386
|
|
|
367
387
|
# Truncate AT_RISK to safe size for issue body (avoid shell quoting issues with large diffs)
|
|
368
388
|
AT_RISK_SNIPPET=$(echo "$AT_RISK" | head -60 | sed "s/'/'\\\\''/g")
|
|
369
389
|
|
|
370
|
-
gh issue create
|
|
390
|
+
gh issue create {GH_FLAG} \
|
|
371
391
|
--title "fix(milestone): rebase conflict during hunk-loss audit for milestone/{slug}" \
|
|
372
392
|
--body "$(cat <<ISSUE_EOF
|
|
373
393
|
## Problem
|
|
374
394
|
|
|
375
|
-
The pre-merge hunk-loss audit for milestone \`{slug}\` detected $AT_RISK_COUNT at-risk staging hunks, then attempted to rebase \`milestone/{slug}\` onto \`origin/
|
|
395
|
+
The pre-merge hunk-loss audit for milestone \`{slug}\` detected $AT_RISK_COUNT at-risk staging hunks, then attempted to rebase \`milestone/{slug}\` onto \`origin/{STAGING_BRANCH}\` to absorb them. The rebase conflicted and was aborted. Manual resolution is required before the milestone can ship.
|
|
376
396
|
|
|
377
397
|
## Root Cause
|
|
378
398
|
|
|
379
|
-
Divergent changes between \`milestone/{slug}\` and \`origin/
|
|
399
|
+
Divergent changes between \`milestone/{slug}\` and \`origin/{STAGING_BRANCH}\` cannot be automatically rebased. Conflicting files must be resolved manually.
|
|
380
400
|
|
|
381
401
|
## Affected Files
|
|
382
402
|
|
|
@@ -401,7 +421,7 @@ ${AT_RISK_SNIPPET}
|
|
|
401
421
|
Resolve the rebase conflict manually:
|
|
402
422
|
\`\`\`bash
|
|
403
423
|
git checkout milestone/{slug}
|
|
404
|
-
git rebase origin/
|
|
424
|
+
git rebase origin/{STAGING_BRANCH}
|
|
405
425
|
# resolve conflicts in the files listed above
|
|
406
426
|
git rebase --continue
|
|
407
427
|
git push origin milestone/{slug} --force-with-lease
|
|
@@ -421,7 +441,7 @@ ISSUE_EOF
|
|
|
421
441
|
# Rebase succeeded — push the rebased branch
|
|
422
442
|
git push origin milestone/{slug} --force-with-lease
|
|
423
443
|
echo "Rebase complete. Milestone branch now includes all $AT_RISK_COUNT at-risk staging hunks."
|
|
424
|
-
git checkout
|
|
444
|
+
git checkout {STAGING_BRANCH}
|
|
425
445
|
fi
|
|
426
446
|
fi
|
|
427
447
|
```
|
|
@@ -430,11 +450,11 @@ fi
|
|
|
430
450
|
|
|
431
451
|
```bash
|
|
432
452
|
# Get the full diff summary (computed after Step 2.5 rebase, so it reflects the final state)
|
|
433
|
-
DIFF_STATS=$(git diff origin/
|
|
434
|
-
COMMIT_LOG=$(git log origin/
|
|
453
|
+
DIFF_STATS=$(git diff origin/{STAGING_BRANCH}...origin/milestone/{slug} --stat)
|
|
454
|
+
COMMIT_LOG=$(git log origin/{STAGING_BRANCH}..origin/milestone/{slug} --oneline)
|
|
435
455
|
|
|
436
|
-
gh pr create \
|
|
437
|
-
--base
|
|
456
|
+
gh pr create {GH_FLAG} \
|
|
457
|
+
--base {STAGING_BRANCH} \
|
|
438
458
|
--head milestone/{slug} \
|
|
439
459
|
--title "Ship: {MILESTONE_TITLE}" \
|
|
440
460
|
--body "$(cat <<'PR_EOF'
|
|
@@ -446,8 +466,8 @@ gh pr create \
|
|
|
446
466
|
{list of all closed issues in this milestone with PR references}
|
|
447
467
|
|
|
448
468
|
## Hunk-Loss Audit
|
|
449
|
-
{If Step 2.5 rebase ran: "✓ Rebase completed — {AT_RISK_COUNT} at-risk staging hunks absorbed into milestone branch before this PR was created. All staging content is preserved."}
|
|
450
|
-
{If Step 2.5 found no at-risk hunks: "✓ Clean — no at-risk staging hunks detected. Safe to merge."}
|
|
469
|
+
{If Step 2.5 rebase ran: "✓ Rebase completed — {AT_RISK_COUNT} at-risk staging-branch hunks absorbed into milestone branch before this PR was created. All staging content is preserved."}
|
|
470
|
+
{If Step 2.5 found no at-risk hunks: "✓ Clean — no at-risk staging-branch hunks detected. Safe to merge."}
|
|
451
471
|
|
|
452
472
|
> ⚠ **IMPORTANT: Merge this PR using a MERGE COMMIT, not squash.** Squash-merging a milestone PR can silently drop staging-only hunks in files the milestone touches. Use the "Create a merge commit" option in the GitHub UI, or `gh pr merge {PR_NUMBER} --merge`.
|
|
453
473
|
|
|
@@ -544,19 +564,20 @@ To deploy: merge `staging → main` via GitHub web UI when ready.
|
|
|
544
564
|
|
|
545
565
|
## Action: Sync Milestone Branch
|
|
546
566
|
|
|
547
|
-
Syncs the milestone branch with the latest
|
|
567
|
+
Syncs the milestone branch with the latest default branch to pick up fast-lane fixes. Run periodically on long-lived milestones.
|
|
548
568
|
|
|
549
569
|
```bash
|
|
550
|
-
cd {REPO_PATH}
|
|
551
|
-
|
|
570
|
+
cd {REPO_PATH}
|
|
571
|
+
DEFAULT_BRANCH=$(yq '.branches.default' forge.yaml)
|
|
572
|
+
git fetch origin $DEFAULT_BRANCH milestone/{slug}
|
|
552
573
|
git checkout milestone/{slug}
|
|
553
|
-
git merge origin
|
|
574
|
+
git merge origin/$DEFAULT_BRANCH --no-edit
|
|
554
575
|
# If conflicts, report to user and STOP
|
|
555
576
|
git push origin milestone/{slug}
|
|
556
|
-
git checkout
|
|
577
|
+
git checkout $DEFAULT_BRANCH
|
|
557
578
|
```
|
|
558
579
|
|
|
559
|
-
Report: "Synced milestone/{slug} with latest
|
|
580
|
+
Report: "Synced milestone/{slug} with latest `{DEFAULT_BRANCH}`. {N} new commits incorporated."
|
|
560
581
|
|
|
561
582
|
---
|
|
562
583
|
|