forgedock 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,455 +1,636 @@
1
1
  ---
2
- description: init-enrich skill backend — consumes a ConfigDraft, deeply infers hard sections from codebase + GitHub, returns enriched ConfigDraft with raised confidences
3
- argument-hint: <ConfigDraft JSON>
2
+ description: AI-powered forge.yaml config generator — scans codebase, queries GitHub, auto-fills all optional sections from detection
3
+ argument-hint: [--preserve | --interactive | --section <name>]
4
4
  ---
5
5
 
6
- # /forgedock-init — init-enrich Skill Backend
6
+ # /forgedock-init — AI-Powered Config Generator
7
7
 
8
8
  **Input**: $ARGUMENTS
9
9
 
10
- You are the **init-enrich skill backend** for ForgeDock's intelligent onboarding pipeline. You consume a `ConfigDraft` JSON object produced by `init-detect`, deeply infer the hard sections (`project_board`, `repos.satellites`, `review`, `verification`) by scanning the codebase and querying GitHub, and return an enriched `ConfigDraft` with the same leaf shape and raised confidences.
11
-
12
- **This is a backend, not a wizard.** You MUST NOT ask the user any interactive questions. You MUST NOT write any files. Unknowable items are represented as `low`-confidence TODO-flagged fields — never as blocking prompts.
10
+ You complete the `forge.yaml` configuration that `npx forgedock init` started. The CLI generates required sections (project, paths, branches) from auto-detection. Your job is to scan the codebase, query GitHub APIs, and produce a complete `forge.yaml` with every applicable optional section filled — using detected values directly, without asking for confirmation.
13
11
 
14
12
  **Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
15
13
  **NEVER use plan mode (EnterPlanMode).**
16
14
 
17
15
  ---
18
16
 
19
- ## ConfigDraft Contract
17
+ ## Argument Parsing
20
18
 
21
- Every leaf in a `ConfigDraft` has the shape:
19
+ | Flag | Effect |
20
+ |------|--------|
21
+ | (none) | Fill all optional sections — overwrite existing values with newly detected ones, skip sections with nothing detected |
22
+ | `--preserve` | Keep existing active section values unchanged — only fill sections that are not yet configured |
23
+ | `--interactive` | Ask confirmation for every detected value and present menus for optional features |
24
+ | `--section <name>` | Fill only one section: `repos`, `project_board`, `services`, `review`, or `verification` |
22
25
 
23
- ```json
24
- { "value": "<string>", "confidence": "high|medium|low", "source": "<string>", "why": "<string>" }
26
+ Parse `$ARGUMENTS` and set:
27
+ ```
28
+ PRESERVE = true if --preserve present
29
+ INTERACTIVE = true if --interactive present (restores full questionnaire behavior for all sections)
30
+ TARGET_SECTION = value from --section <name>, or "all"
25
31
  ```
26
32
 
27
- Confidence levels:
28
- - `high` — verified from a concrete, unambiguous source
29
- - `medium` — inferred from available signals; likely correct
30
- - `low` — guessed default or not found; caller should surface as a `# TODO(forgedock:<field>)` flag
31
-
32
- This skill extends the draft by adding optional section fields. It never removes or downgrades existing fields. Required sections (project, paths, branches) are passed through unchanged.
33
-
34
- ---
35
-
36
- ## Phase 0: Parse Input and Extract Identity
37
-
38
- ### 0A: Parse ConfigDraft from $ARGUMENTS
33
+ If both `--preserve` and `--interactive` are present, `--interactive` takes precedence (`PRESERVE = false`).
39
34
 
40
- `$ARGUMENTS` contains a ConfigDraft JSON string. Parse it:
35
+ If `--section` was provided, validate the value immediately:
41
36
 
42
- ```
43
- DRAFT = JSON.parse($ARGUMENTS)
37
+ ```bash
38
+ ALLOWED_SECTIONS="repos project_board services review verification"
39
+ if [ "$TARGET_SECTION" != "all" ]; then
40
+ VALID=false
41
+ for s in $ALLOWED_SECTIONS; do
42
+ [ "$TARGET_SECTION" = "$s" ] && VALID=true && break
43
+ done
44
+ if [ "$VALID" = "false" ]; then
45
+ echo "Error: '$TARGET_SECTION' is not a valid section name."
46
+ echo ""
47
+ echo "Valid sections: repos, project_board, services, review, verification"
48
+ echo ""
49
+ echo "Usage: /forgedock-init --section <name>"
50
+ exit 1
51
+ fi
52
+ fi
44
53
  ```
45
54
 
46
- If `$ARGUMENTS` is empty or not valid JSON, output an error and exit:
55
+ ---
47
56
 
48
- ```
49
- Error: /forgedock-init expects a ConfigDraft JSON string as its argument.
57
+ ## Phase 1: Load forge.yaml
50
58
 
51
- Usage:
52
- The init-detect module produces the ConfigDraft. Example:
53
- node -e "import('./bin/init-detect.mjs').then(m => m.detectConfig()).then(d => console.log(JSON.stringify(d)))"
59
+ ### 1A: Locate and read
54
60
 
55
- Pass the output as the argument to /forgedock-init.
61
+ ```bash
62
+ FORGE_YAML="$(pwd)/forge.yaml"
63
+ if [ ! -f "$FORGE_YAML" ]; then
64
+ echo "forge.yaml not found in current directory."
65
+ echo ""
66
+ echo "Run first: npx forgedock init"
67
+ echo ""
68
+ echo "This generates the required skeleton (project, paths, branches)."
69
+ echo "Then re-run /forgedock-init to complete the optional sections."
70
+ exit 1
71
+ fi
72
+ cat "$FORGE_YAML"
56
73
  ```
57
74
 
58
- ### 0B: Extract identity fields
75
+ ### 1B: Extract required section values
59
76
 
60
- Extract these from the incoming draft for use in all GitHub queries:
77
+ Parse these from the existing file — they are needed for all subsequent queries:
61
78
 
62
79
  ```
63
- OWNER = DRAFT.project.owner.value # GitHub org or username
64
- REPO = DRAFT.project.repo.value # Repository name
65
- REPO_ROOT = DRAFT.paths.root.value # Absolute path to repo root
80
+ OWNER = project.owner
81
+ REPO = project.repo
82
+ REPO_ROOT = paths.root
66
83
  ```
67
84
 
68
- Validate that OWNER and REPO are non-empty and not placeholder values before proceeding with any GitHub queries:
69
-
70
- ```
71
- if OWNER is empty or equals "your-github-org":
72
- set OWNER = null # GitHub queries will be skipped
73
- note: "project.owner is a placeholder — board and satellite enrichment skipped"
85
+ After extracting these values, validate that OWNER and REPO are non-empty and do not contain placeholder values. If placeholders are found, attempt auto-detection from the git remote before erroring:
74
86
 
75
- if REPO is empty or equals "your-repo-name":
76
- set REPO = null
77
- note: "project.repo is a placeholder — satellite enrichment skipped"
87
+ ```bash
88
+ OWNER_IS_PLACEHOLDER=false
89
+ REPO_IS_PLACEHOLDER=false
90
+ [ -z "$OWNER" ] || [ "$OWNER" = "your-github-org" ] && OWNER_IS_PLACEHOLDER=true
91
+ [ -z "$REPO" ] || [ "$REPO" = "your-repo-name" ] && REPO_IS_PLACEHOLDER=true
92
+
93
+ if [ "$OWNER_IS_PLACEHOLDER" = "true" ] || [ "$REPO_IS_PLACEHOLDER" = "true" ]; then
94
+ echo "Placeholder values detected in forge.yaml — attempting auto-detection from git remote..."
95
+
96
+ REMOTE_URL=$(git -C "$REPO_ROOT" remote get-url origin 2>/dev/null || true)
97
+ DETECTED_OWNER=""
98
+ DETECTED_REPO=""
99
+
100
+ if [ -n "$REMOTE_URL" ]; then
101
+ # SSH format: git@github.com:owner/repo.git
102
+ if echo "$REMOTE_URL" | grep -qE '^git@[^:]+:'; then
103
+ DETECTED_OWNER=$(echo "$REMOTE_URL" | sed -E 's|^git@[^:]+:([^/]+)/.*|\1|')
104
+ DETECTED_REPO=$(echo "$REMOTE_URL" | sed -E 's|^git@[^:]+:[^/]+/||; s|\.git$||')
105
+ # HTTPS format: https://github.com/owner/repo.git
106
+ elif echo "$REMOTE_URL" | grep -qE '^https?://'; then
107
+ DETECTED_OWNER=$(echo "$REMOTE_URL" | sed -E 's|^https?://[^/]+/([^/]+)/.*|\1|')
108
+ DETECTED_REPO=$(echo "$REMOTE_URL" | sed -E 's|^https?://[^/]+/[^/]+/||; s|\.git$||')
109
+ fi
110
+ fi
111
+
112
+ if [ -n "$DETECTED_OWNER" ] && [ -n "$DETECTED_REPO" ]; then
113
+ echo "Auto-detected: owner=\"$DETECTED_OWNER\" repo=\"$DETECTED_REPO\""
114
+ # Update forge.yaml in-place with detected values
115
+ [ "$OWNER_IS_PLACEHOLDER" = "true" ] && \
116
+ sed -i '/^project:/,/^[a-z]/{s|^ owner:.*| owner: "'"$DETECTED_OWNER"'"|}' "$FORGE_YAML"
117
+ [ "$REPO_IS_PLACEHOLDER" = "true" ] && \
118
+ sed -i "s|^ repo:.*| repo: \"$DETECTED_REPO\"|" "$FORGE_YAML"
119
+ echo "forge.yaml updated with auto-detected values. Continuing..."
120
+ # Re-read the corrected values
121
+ OWNER="$DETECTED_OWNER"
122
+ REPO="$DETECTED_REPO"
123
+ else
124
+ echo "Error: project.owner/project.repo contain placeholder values and auto-detection failed."
125
+ echo ""
126
+ echo "Could not detect owner/repo from git remote. Open forge.yaml and fill in the 'project:' section:"
127
+ echo " project:"
128
+ echo " owner: \"your-actual-github-org-or-username\""
129
+ echo " repo: \"your-actual-repo-name\""
130
+ echo ""
131
+ echo "Then re-run /forgedock-init."
132
+ exit 1
133
+ fi
134
+ fi
78
135
  ```
79
136
 
80
- These are non-blocking — an incomplete draft still gets codebase enrichment (review, verification).
137
+ ### 1C: Detect already-configured optional sections
81
138
 
82
- ### 0C: Check GitHub auth
139
+ For each optional section, check if it has an UNCOMMENTED entry in the file. A commented-out section (all lines starting with `#`) is NOT configured.
83
140
 
84
141
  ```bash
85
- gh auth status 2>&1
142
+ # Check which optional sections are already active (uncommented)
143
+ grep -q "^repos:" "$FORGE_YAML" && HAS_REPOS=true || HAS_REPOS=false
144
+ grep -q "^project_board:" "$FORGE_YAML" && HAS_PROJECT_BOARD=true || HAS_PROJECT_BOARD=false
145
+ grep -q "^services:" "$FORGE_YAML" && HAS_SERVICES=true || HAS_SERVICES=false
146
+ grep -q "^review:" "$FORGE_YAML" && HAS_REVIEW=true || HAS_REVIEW=false
147
+ grep -q "^verification:" "$FORGE_YAML" && HAS_VERIFICATION=true || HAS_VERIFICATION=false
86
148
  ```
87
149
 
88
- Set `GH_AUTHED = true` if the output does NOT contain "not logged in" or "You are not logged into". Set `GH_AUTHED = false` otherwise.
150
+ If `PRESERVE=true` and a section is already active, skip it (unless `--section` targets it explicitly). For skipped sections, tell the user: `{section}: already configured — skipped (--preserve). Use /forgedock-init --section {section} to update.`
89
151
 
90
- If `GH_AUTHED = false` or `OWNER = null`: GitHub-dependent enrichment phases (1, 2) will be skipped; their fields will be set to `low` confidence with a TODO. Codebase-only phases (3, 4) still run.
152
+ If `PRESERVE=false` (the default) and a section is already active, it will be overwritten with newly detected values in Phase 5C — no prompt needed.
91
153
 
92
154
  ---
93
155
 
94
- ## Phase 1: Enrich project_board
156
+ ## Phase 2: Codebase Scan
95
157
 
96
- **Skip if**: `GH_AUTHED = false` or `OWNER = null`.
158
+ Run these scans unconditionally — they populate defaults that reduce the number of clarifying questions needed.
97
159
 
98
- ### 1A: Discover project boards
160
+ ### 2A: Tech stack detection
99
161
 
100
162
  ```bash
101
- gh project list --owner "$OWNER" --format json 2>/dev/null \
102
- | jq '[.projects[] | {number: .number, title: .title, id: .id}]'
103
- ```
104
-
105
- If the query fails or returns an empty list: skip Phase 1 entirely. Add a `low`-confidence placeholder for the entire `project_board` section (see 1E).
163
+ # Node.js / TypeScript
164
+ [ -f "$REPO_ROOT/package.json" ] && \
165
+ cat "$REPO_ROOT/package.json" | grep -E '"name"|"dependencies"|"devDependencies"' | head -30
106
166
 
107
- If one project is found: select it automatically (high confidence — no ambiguity).
108
- If multiple projects are found: select the first one (medium confidence — note that the first result may not be the right project board).
167
+ # Python
168
+ [ -f "$REPO_ROOT/pyproject.toml" ] && grep -E "^\[project\]|^name|^requires-python|^dependencies" "$REPO_ROOT/pyproject.toml" | head -20
169
+ [ -f "$REPO_ROOT/requirements.txt" ] && head -20 "$REPO_ROOT/requirements.txt"
109
170
 
110
- Record:
111
- ```
112
- PROJECT_NUMBER = first (or only) project number
113
- PROJECT_TITLE = project title
114
- PROJECT_ID = project id (the PVT_... node ID)
115
- ```
171
+ # Rust
172
+ [ -f "$REPO_ROOT/Cargo.toml" ] && grep -E "^\[package\]|^name|^edition|^\[dependencies\]" "$REPO_ROOT/Cargo.toml" | head -20
116
173
 
117
- ### 1B: Fetch field definitions
174
+ # Ruby
175
+ [ -f "$REPO_ROOT/Gemfile" ] && head -15 "$REPO_ROOT/Gemfile"
118
176
 
119
- ```bash
120
- gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json 2>/dev/null \
121
- | jq '[.fields[] | {name: .name, id: .id, type: .type, options: (.options // [])}]'
177
+ # Go
178
+ [ -f "$REPO_ROOT/go.mod" ] && head -10 "$REPO_ROOT/go.mod"
122
179
  ```
123
180
 
124
- If the query fails: skip Phase 1, emit low-confidence placeholder.
125
-
126
- ### 1C: Map fields to ForgeDock slots
127
-
128
- For each of the five ForgeDock field slots, search the field list case-insensitively:
129
-
130
- | ForgeDock slot | Primary match | Fallback match |
131
- |----------------|---------------|----------------|
132
- | `status` | "Status" | "State" |
133
- | `lane` | "Lane" | "Track", "Type" |
134
- | `component` | "Component" | "Area", "Team" |
135
- | `priority` | "Priority" | "Severity" |
136
- | `workflow` | "Workflow" | "Stage", "Phase" |
181
+ Build `TECH_STACK` description from findings (e.g., "Next.js 15, FastAPI, PostgreSQL").
137
182
 
138
- For each slot:
139
- - If a field is found: confidence = `high`, source = `gh project field-list`, why = `Field named "<name>" (id: <id>) found in project "<PROJECT_TITLE>"`
140
- - If no field matches: confidence = `low`, value = `""`, why = `No field matching "<slot>" found in project "<PROJECT_TITLE>"; add a field with this name and re-run /forgedock-init`
183
+ ### 2B: Project context
141
184
 
142
- ### 1D: Map option IDs
143
-
144
- For each single-select field that was successfully mapped, extract its option IDs.
145
-
146
- For each of the required option slots, match case-insensitively:
185
+ ```bash
186
+ # CLAUDE.md — primary context source
187
+ [ -f "$REPO_ROOT/CLAUDE.md" ] && cat "$REPO_ROOT/CLAUDE.md"
147
188
 
148
- **status options**: "Todo" / "To Do" → `todo`; "In Progress" / "In-Progress" → `in_progress`; "Done" / "Complete" → `done`
189
+ # README.md — fallback
190
+ [ -f "$REPO_ROOT/README.md" ] && head -60 "$REPO_ROOT/README.md"
191
+ ```
149
192
 
150
- **lane options**: "Fast" / "Fast Lane" → `fast`; "Feature" / "Feature Lane" → `feature`; "Sync" → `sync`
193
+ Extract a 2–4 sentence project description for `review.context`.
151
194
 
152
- **priority options**: "P0" / "Critical" → `p0`; "P1" / "High" → `p1`; "P2" / "Medium" → `p2`; "P3" / "Low" → `p3`
195
+ ### 2C: Docker / service names
153
196
 
154
- **workflow options**: "Investigating" → `investigating`; "Building" → `building`; "In Review" / "In-Review" → `in_review`; "Merged" / "Complete" → `merged`
197
+ ```bash
198
+ # Container name prefixes for verification section
199
+ ls "$REPO_ROOT"/docker-compose*.yml 2>/dev/null
200
+ for f in "$REPO_ROOT"/docker-compose*.yml; do
201
+ [ -f "$f" ] || continue
202
+ grep -E "^\s+container_name:|^\s+image:" "$f" | head -20
203
+ done
204
+ ```
155
205
 
156
- For each option:
157
- - If matched: confidence = `high`, source = `gh project field-list (options)`, why = `Option "<name>" (id: <id>) found in field "<field_name>"`
158
- - If not matched: confidence = `low`, value = `""`, why = `No option matching "<slot>" found in field "<field_name>"; add the option or set this value manually`
206
+ Extract container name prefixes (e.g., `acme-`, `my-app-`) for `verification.internal_service_patterns` and `verification.services`.
159
207
 
160
- ### 1E: Build project_board enrichment
208
+ ### 2D: Branch topology
161
209
 
162
- Assemble the enriched `project_board` section of the draft. If Phase 1 was skipped entirely:
210
+ ```bash
211
+ # Staging branch detection
212
+ git -C "$REPO_ROOT" branch -r | grep -E "staging|develop|dev" | head -5
163
213
 
164
- ```json
165
- "project_board": {
166
- "owner": { "value": "", "confidence": "low", "source": "skipped", "why": "GitHub auth unavailable or project.owner is a placeholder; run gh auth login then re-run /forgedock-init" },
167
- "project_number": { "value": "", "confidence": "low", "source": "skipped", "why": "GitHub auth unavailable; project board not queried" },
168
- "project_id": { "value": "", "confidence": "low", "source": "skipped", "why": "GitHub auth unavailable; obtain via: gh project list --owner <owner> --format json | jq '.projects[] | .id' (value must start with PVT_)" },
169
- "field_ids": { ... all slots at low confidence ... },
170
- "option_ids": { ... all option slots at low confidence ... }
171
- }
214
+ # CI/CD target branches
215
+ grep -rh "branches:" "$REPO_ROOT/.github/workflows/"*.yml 2>/dev/null | head -20
172
216
  ```
173
217
 
174
- **Important**: `project_board.project_id` must be a GitHub Projects v2 node ID with the `PVT_` prefix (e.g. `PVT_kwHOxxxxxxxxxxxxxxxx`). When emitting this field at high confidence, always include a note in `why` reminding the caller of this format requirement.
175
-
176
- ---
177
-
178
- ## Phase 2: Enrich repos.satellites
179
-
180
- **Skip if**: `GH_AUTHED = false` or `OWNER = null` or `REPO = null`.
218
+ Use to confirm or suggest `branches.staging`.
181
219
 
182
- ### 2A: List sibling repos
220
+ ### 2E: API health endpoint hints
183
221
 
184
222
  ```bash
185
- gh repo list "$OWNER" --limit 50 --json name,description,primaryLanguage 2>/dev/null \
186
- | jq '[.[] | select(.name != "'"$REPO"'") | {name: .name, description: (.description // ""), language: (.primaryLanguage.name // "")}]'
223
+ # Look for health route definitions
224
+ grep -rn "health\|/ping\|/status" "$REPO_ROOT" --include="*.py" --include="*.ts" --include="*.go" -l 2>/dev/null | head -10
225
+ grep -rn "healthcheck\|health_endpoint\|HEALTH_URL" "$REPO_ROOT" --include="*.yml" --include="*.yaml" -l 2>/dev/null | head -5
187
226
  ```
188
227
 
189
- If the query fails or returns empty: skip Phase 2, emit low-confidence placeholder.
190
-
191
- ### 2B: Infer satellite candidates
228
+ Build `DETECTED_HEALTH_PATH` from findings (e.g., `/health`, `/api/health`). If nothing found, leave empty.
192
229
 
193
- For each sibling repo, apply heuristics to determine if it looks like a ForgeDock satellite (i.e., a repo the user might want to route issues to with a short prefix):
230
+ ### 2F: Analytics/services detection
194
231
 
195
- **Include as a satellite candidate if any of these match**:
196
- - Name ends with `-sdk`, `-api`, `-mcp`, `-cli`, `-server`, `-worker`, `-plugin`
197
- - Name contains `sdk`, `client`, `plugin`, `extension`, `lib`
198
- - Description mentions "companion", "client for", "SDK for", "plugin for", "server for"
199
-
200
- **Exclude**:
201
- - Names that are clearly personal/fork repos (contain the word "fork", start with the owner name exactly)
202
- - Repos with zero description and language = null (likely empty/template repos)
203
-
204
- For each candidate, derive a prefix:
205
- - Strip the `$OWNER-` prefix from the name if present (e.g. `acme-mcp-server` → `mcp-server`)
206
- - Take the first word before any `-` (e.g. `mcp-server` → `mcp`)
207
- - If the prefix conflicts with another candidate, use the first two words
208
-
209
- ### 2C: Build repos enrichment
232
+ ```bash
233
+ # Umami — look for self-hosted instance config or tracking script
234
+ grep -rn "umami\|UMAMI_WEBSITE_ID\|umami.is" "$REPO_ROOT" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.env*" -l 2>/dev/null | head -5
210
235
 
211
- For each satellite candidate:
236
+ # Microsoft Clarity
237
+ grep -rn "clarity\|CLARITY_PROJECT_ID\|clarity.ms" "$REPO_ROOT" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.html" -l 2>/dev/null | head -5
212
238
 
213
- ```json
214
- {
215
- "prefix": { "value": "<derived-prefix>", "confidence": "medium", "source": "gh repo list + name heuristic", "why": "Repo '<name>' looks like a satellite (name pattern: <pattern>); verify prefix is correct" },
216
- "repo": { "value": "<owner>/<name>", "confidence": "high", "source": "gh repo list", "why": "Repo exists under owner <owner>" },
217
- "staging_branch": { "value": "main", "confidence": "low", "source": "default", "why": "Could not verify staging branch without reading satellite repo; defaulting to 'main'" }
218
- }
239
+ # Google Analytics 4
240
+ grep -rn "G-[A-Z0-9]\+\|GA4\|gtag\|NEXT_PUBLIC_GA_ID\|GA_MEASUREMENT_ID" "$REPO_ROOT" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.env*" -l 2>/dev/null | head -5
219
241
  ```
220
242
 
221
- If no candidates found:
222
-
223
- ```json
224
- "repos": {
225
- "satellites": [],
226
- "_enrichment_note": { "value": "none", "confidence": "low", "source": "gh repo list", "why": "No repos under <owner> matched satellite heuristics; if satellites exist, add them manually to forge.yaml" }
227
- }
228
- ```
243
+ Build `DETECTED_ANALYTICS` list from findings:
244
+ - `DETECTED_UMAMI=true` if umami references found
245
+ - `DETECTED_CLARITY=true` if Clarity references found
246
+ - `DETECTED_GA4=true` if GA4 references found
247
+ - If none found, `DETECTED_ANALYTICS_EMPTY=true`
229
248
 
230
249
  ---
231
250
 
232
- ## Phase 3: Enrich review
251
+ ## Phase 3: GitHub Queries
233
252
 
234
- **Always runs** — does not require GitHub auth.
253
+ Run these to gather live IDs. Failures are non-blocking — skip the section if the query fails.
235
254
 
236
- ### 3A: Detect tech stack
255
+ ### 3A: Check GitHub auth
237
256
 
238
- For each supported ecosystem, check for its manifest file under `$REPO_ROOT`:
239
-
240
- **Node.js / TypeScript**:
241
257
  ```bash
242
- if [ -f "$REPO_ROOT/package.json" ]; then
243
- # Extract name, framework dependencies
244
- cat "$REPO_ROOT/package.json" | jq '{
245
- name: .name,
246
- deps: ((.dependencies // {}) + (.devDependencies // {})) | keys | map(select(
247
- test("next|react|vue|nuxt|svelte|express|fastify|hono|nest|koa|astro|remix|gatsby|vite|esbuild|rollup|webpack|prisma|drizzle|sequelize|typeorm|mongoose")
248
- ))
249
- }'
250
- fi
258
+ gh auth status 2>&1
251
259
  ```
252
260
 
253
- **Python**:
254
- ```bash
255
- [ -f "$REPO_ROOT/pyproject.toml" ] && grep -E "^(name|requires-python)" "$REPO_ROOT/pyproject.toml" | head -5
256
- [ -f "$REPO_ROOT/pyproject.toml" ] && grep -E "fastapi|django|flask|starlette|sqlalchemy|pydantic|alembic|celery|aiohttp" "$REPO_ROOT/pyproject.toml" | head -10
257
- [ -f "$REPO_ROOT/requirements.txt" ] && grep -iE "fastapi|django|flask|starlette|sqlalchemy|pydantic|alembic|celery|aiohttp" "$REPO_ROOT/requirements.txt" | head -10
258
- ```
261
+ If not authenticated, skip all GitHub queries and note which sections will need manual completion.
259
262
 
260
- **Rust**:
261
- ```bash
262
- [ -f "$REPO_ROOT/Cargo.toml" ] && grep -E "^name|axum|actix|rocket|tokio|serde|sqlx" "$REPO_ROOT/Cargo.toml" | head -10
263
- ```
263
+ ### 3B: Project board discovery
264
264
 
265
- **Go**:
266
265
  ```bash
267
- [ -f "$REPO_ROOT/go.mod" ] && head -10 "$REPO_ROOT/go.mod"
266
+ # List projects owned by this org/user
267
+ gh project list --owner "$OWNER" --format json 2>/dev/null \
268
+ | jq '.projects[] | {number: .number, title: .title, id: .id}'
268
269
  ```
269
270
 
270
- **Ruby**:
271
- ```bash
272
- [ -f "$REPO_ROOT/Gemfile" ] && grep -E "rails|sinatra|roda|hanami|sidekiq" "$REPO_ROOT/Gemfile" | head -5
273
- ```
271
+ Store results in `FOUND_PROJECT_BOARDS`. Do NOT ask the user at this stage — Phase 4B decides based on the count of boards found. If exactly one is found, it will be used automatically. If multiple are found, Phase 4B will ask once. Record:
272
+ - `PROJECT_NUMBER`
273
+ - `PROJECT_ID` (the `PVT_...` node ID)
274
+
275
+ ### 3C: Project board field IDs
274
276
 
275
- **Docker / infrastructure**:
276
277
  ```bash
277
- ls "$REPO_ROOT"/docker-compose*.yml 2>/dev/null | head -5
278
- [ -f "$REPO_ROOT/Dockerfile" ] && head -5 "$REPO_ROOT/Dockerfile"
278
+ # Only run if PROJECT_NUMBER is set
279
+ gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json 2>/dev/null \
280
+ | jq '.fields[] | {name: .name, id: .id, type: .type}'
279
281
  ```
280
282
 
281
- Build a concise `TECH_STACK` string from the detected components, e.g. "Next.js 15, FastAPI, PostgreSQL 16, Docker". If multiple ecosystems are detected, list all. If nothing is detected, set `TECH_STACK = ""`.
282
-
283
- **Confidence**:
284
- - One or more manifest files found → `medium` (detected from deps but not verified running)
285
- - Nothing detected → `low`
286
-
287
- ### 3B: Extract project context
283
+ Map field names to their IDs:
284
+ - Look for fields named `Status`, `Lane`, `Component`, `Priority`, `Workflow` (case-insensitive)
285
+ - Record their `PVTSSF_...` IDs
288
286
 
289
287
  ```bash
290
- # Primary: CLAUDE.md
291
- if [ -f "$REPO_ROOT/CLAUDE.md" ]; then
292
- cat "$REPO_ROOT/CLAUDE.md"
293
- fi
294
-
295
- # Fallback: README.md (first 80 lines)
296
- if [ ! -f "$REPO_ROOT/CLAUDE.md" ] && [ -f "$REPO_ROOT/README.md" ]; then
297
- head -80 "$REPO_ROOT/README.md"
298
- fi
288
+ # Get option IDs for single-select fields
289
+ gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json 2>/dev/null \
290
+ | jq '.fields[] | select(.type == "SINGLE_SELECT") | {name: .name, options: [.options[] | {name: .name, id: .id}]}'
299
291
  ```
300
292
 
301
- From the content, synthesize a 2–4 sentence `REVIEW_CONTEXT` that captures:
302
- - What the project does
303
- - Its architecture (if mentioned)
304
- - Any deploy or infrastructure notes relevant to a code reviewer
305
- - Known conventions or pitfalls (if mentioned)
293
+ Map option names to IDs for `status` (Todo/In Progress/Done), `lane` (Fast/Feature/Sync), `priority` (P0/P1/P2/P3), `workflow` (Investigating/Building/In Review/Merged).
306
294
 
307
- If neither CLAUDE.md nor README.md exists: `REVIEW_CONTEXT = ""` at low confidence.
295
+ ### 3D: Satellite repo discovery
308
296
 
309
- **Confidence**:
310
- - Synthesized from CLAUDE.md → `high`
311
- - Synthesized from README.md → `medium`
312
- - No source found → `low`
313
-
314
- ### 3C: Build review enrichment
315
-
316
- ```json
317
- "review": {
318
- "tech_stack": { "value": "<TECH_STACK>", "confidence": "<high|medium|low>", "source": "<manifest files found>", "why": "<what was detected>" },
319
- "context": { "value": "<REVIEW_CONTEXT>", "confidence": "<high|medium|low>", "source": "<CLAUDE.md|README.md|none>", "why": "<synthesis note>" }
320
- }
297
+ ```bash
298
+ # List all repos under this owner
299
+ gh repo list "$OWNER" --limit 50 --json name,description,url 2>/dev/null \
300
+ | jq '.[] | select(.name != "'"$REPO"'") | {name: .name, description: .description}'
321
301
  ```
322
302
 
323
- ---
303
+ Store results in `SIBLING_REPOS`. Assess satellite signals for each repo — strong signals are: separate CI workflow files, separate `package.json`, separate staging branch. Do NOT ask the user at this stage — Phase 4A decides whether to ask based on signal strength.
324
304
 
325
- ## Phase 4: Enrich verification
305
+ ---
326
306
 
327
- **Always runs** — does not require GitHub auth.
307
+ ## Phase 4: Resolve Values for Optional Sections
308
+
309
+ **Decision rule** (applies to every sub-phase below):
310
+ - **Auto-detected value** → use it directly. No confirmation prompt.
311
+ - **Genuine ambiguity** (e.g., multiple project boards, multiple candidates for the same field) → ask once with the detected candidates listed.
312
+ - **Nothing detected** → skip the section silently. Do NOT offer a menu or ask "do you want X?".
313
+ - **`--interactive` flag** → override the above and ask confirmation/selection for every section, even when detection found a clear answer.
314
+
315
+ ### 4A: repos section
316
+
317
+ **Determine `SATELLITE_CANDIDATES`**: From `SIBLING_REPOS` (Phase 3D), filter to repos with strong satellite signals — repos that have their own CI workflow files, separate `package.json`, or a staging branch distinct from this repo's staging branch. Repos without any of these signals are NOT satellite candidates.
318
+
319
+ **Decision**:
320
+ - `SATELLITE_CANDIDATES` is non-empty AND `INTERACTIVE=false` → log detected candidates but do NOT configure satellites automatically (routing prefix assignments require human intent). Ask once:
321
+ ```
322
+ Detected potential satellite repos with separate CI/branches:
323
+ - {repo_name}: {description}
324
+ (...)
325
+
326
+ Do any of these serve as ForgeDock routing satellites? Enter prefix:repo mappings
327
+ (e.g., "mcp:acme-mcp-server,api:acme-api"), or press Enter to skip:
328
+ ```
329
+ - `SATELLITE_CANDIDATES` is empty AND `INTERACTIVE=false` → skip this section silently.
330
+ - `INTERACTIVE=true` → present all sibling repos and ask which (if any) are satellites.
331
+
332
+ **Rationale**: Satellite routing prefix assignments require deliberate human intent — there is no safe auto-detection heuristic. Ask only when strong signals exist.
333
+
334
+ ### 4B: project_board section
335
+
336
+ **Decision**:
337
+ - Exactly one project board found in 3B → use it. Auto-map field names to IDs. Only ask if field name matching is ambiguous (multiple fields could map to `Status`, etc.).
338
+ - Multiple project boards found → ask which one ForgeDock should use:
339
+ ```
340
+ Multiple project boards found under {OWNER}:
341
+ [1] {TITLE_1} (#{NUMBER_1})
342
+ [2] {TITLE_2} (#{NUMBER_2})
343
+ ...
344
+
345
+ Which board should ForgeDock use? Enter number, or press Enter to skip:
346
+ ```
347
+ - Zero project boards found AND `INTERACTIVE=false` → skip this section silently.
348
+ - `INTERACTIVE=true` → present all boards and ask which one to use.
349
+
350
+ ### 4C: services section
351
+
352
+ **Decision** (based on `DETECTED_ANALYTICS` from Phase 2F):
353
+ - `DETECTED_UMAMI=true` → record Umami; ask for the specific `UMAMI_URL` and `UMAMI_WEBSITE_ID` if not found in env/config files.
354
+ - `DETECTED_CLARITY=true` → record Clarity; ask for `CLARITY_PROJECT_ID` if not found in env/config files.
355
+ - `DETECTED_GA4=true` → record GA4; ask for `GA4_PROPERTY_ID` if not found in env/config files.
356
+ - `DETECTED_ANALYTICS_EMPTY=true` AND `INTERACTIVE=false` → skip this section silently.
357
+ - `INTERACTIVE=true` → present the full analytics menu regardless of detection:
358
+ ```
359
+ Does your project use any of the following analytics/monitoring services?
360
+ [1] Umami (self-hosted)
361
+ [2] Microsoft Clarity
362
+ [3] Google Analytics 4
363
+ [4] None / Skip this section
364
+
365
+ Enter numbers separated by commas (e.g., "1,3"), or "4" to skip:
366
+ ```
367
+
368
+ **API URL for health checks** (services.api_url):
369
+ - `DETECTED_HEALTH_PATH` is set (from Phase 2E) AND a domain is known → compose `API_URL` directly. Do not ask.
370
+ - Nothing detected AND `INTERACTIVE=false` → leave `api_url` empty in the section.
371
+ - `INTERACTIVE=true` → ask:
372
+ ```
373
+ Do you have an API URL for health checks? (e.g., https://api.myproject.io)
374
+ Enter URL or press Enter to skip:
375
+ ```
376
+
377
+ ### 4D: review section
378
+
379
+ **Decision**:
380
+ - `TECH_STACK` built in Phase 2A → use it directly as `review.tech_stack`. Do not ask for confirmation.
381
+ - `review.context` → derive from `REPO_ROOT/CLAUDE.md` or README.md (Phase 2B) without prompting. Use the extracted 2–4 sentence description.
382
+ - `INTERACTIVE=true` → show detected value and ask:
383
+ ```
384
+ Detected tech stack: {TECH_STACK}
385
+ Is this accurate? Add/edit if needed, or press Enter to accept:
386
+ ```
387
+ Then ask:
388
+ ```
389
+ Any additional context for code reviewers? (unusual conventions, deploy setup, known pitfalls)
390
+ Press Enter to skip, or describe briefly:
391
+ ```
392
+
393
+ ### 4E: verification section
394
+
395
+ **Decision**:
396
+ - `DETECTED_HEALTH_PATH` is set (from Phase 2E) AND a base domain is known → compose full `HEALTH_ENDPOINT` URL directly. Do not ask.
397
+ - Container name prefixes detected in Phase 2C → use them directly as `internal_service_patterns`. Do not ask for confirmation.
398
+ - Nothing detected for health endpoint AND nothing detected for containers AND `INTERACTIVE=false` → skip this section silently.
399
+ - `INTERACTIVE=true` → show detected values and ask for confirmation:
400
+ ```
401
+ Detected potential health endpoint path: {DETECTED_PATH}
402
+ Full health URL (e.g., https://api.myproject.io/health), or press Enter to skip:
403
+ ```
404
+ ```
405
+ Detected container name prefixes: {PREFIXES}
406
+ Are these correct? Add/edit prefixes, or press Enter to accept:
407
+ ```
328
408
 
329
- ### 4A: Detect health endpoint
409
+ ---
330
410
 
411
+ ## Phase 5: Write forge.yaml
412
+
413
+ ### 5A: Preserve required sections
414
+
415
+ Read the current `forge.yaml` content. Extract the complete `project:`, `paths:`, and `branches:` blocks verbatim — they must not change.
416
+
417
+ ### 5B: Build optional section YAML
418
+
419
+ For each optional section with detected or confirmed values (from Phase 4):
420
+
421
+ **repos** (if satellites confirmed):
422
+ ```yaml
423
+ repos:
424
+ default:
425
+ repo: "{OWNER}/{REPO}"
426
+ staging_branch: "{branches.staging}"
427
+ satellites:
428
+ - prefix: "{PREFIX}"
429
+ repo: "{OWNER}/{SATELLITE_REPO}"
430
+ staging_branch: "main"
431
+ local_path: "{REPO_ROOT}/../{SATELLITE_REPO}"
432
+ ```
433
+
434
+ **project_board** (if board and field IDs found):
435
+ ```yaml
436
+ project_board:
437
+ owner: "{OWNER}"
438
+ project_number: {PROJECT_NUMBER}
439
+ project_id: "{PROJECT_ID}" # Must be a PVT_... node ID — obtain via: gh project list --owner <owner> --format json | jq '.projects[] | .id'
440
+ field_ids:
441
+ status: "{STATUS_FIELD_ID}"
442
+ lane: "{LANE_FIELD_ID}"
443
+ component: "{COMPONENT_FIELD_ID}"
444
+ priority: "{PRIORITY_FIELD_ID}"
445
+ workflow: "{WORKFLOW_FIELD_ID}"
446
+ option_ids:
447
+ status:
448
+ todo: "{TODO_OPTION_ID}"
449
+ in_progress: "{IN_PROGRESS_OPTION_ID}"
450
+ done: "{DONE_OPTION_ID}"
451
+ lane:
452
+ fast: "{FAST_OPTION_ID}"
453
+ feature: "{FEATURE_OPTION_ID}"
454
+ priority:
455
+ p0: "{P0_OPTION_ID}"
456
+ p1: "{P1_OPTION_ID}"
457
+ p2: "{P2_OPTION_ID}"
458
+ p3: "{P3_OPTION_ID}"
459
+ workflow:
460
+ investigating: "{INVESTIGATING_OPTION_ID}"
461
+ building: "{BUILDING_OPTION_ID}"
462
+ in_review: "{IN_REVIEW_OPTION_ID}"
463
+ merged: "{MERGED_OPTION_ID}"
464
+ ```
465
+
466
+ **services** (if analytics/monitoring confirmed):
467
+ ```yaml
468
+ services:
469
+ domain: "{ROOT_DOMAIN}"
470
+ api_url: "{API_URL}"
471
+ analytics:
472
+ # Only include subsections detected (or selected in --interactive mode)
473
+ umami: # if selected
474
+ url: "{UMAMI_URL}"
475
+ website_id: "{UMAMI_WEBSITE_ID}"
476
+ clarity: # if selected
477
+ project_id: "{CLARITY_PROJECT_ID}"
478
+ ga4: # if selected
479
+ property_id: "{GA4_PROPERTY_ID}"
480
+ ```
481
+
482
+ **review** (if tech stack or context provided):
483
+ ```yaml
484
+ review:
485
+ tech_stack: "{TECH_STACK}"
486
+ context: |
487
+ {PROJECT_CONTEXT}
488
+ ```
489
+
490
+ **verification** (if health endpoint or containers found):
491
+ ```yaml
492
+ verification:
493
+ health_endpoint: "{HEALTH_ENDPOINT}"
494
+ health_patterns:
495
+ - '"status": "ok"'
496
+ internal_service_patterns:
497
+ - "{SERVICE_PREFIX}"
498
+ ```
499
+
500
+ ### 5C: Section merge strategy
501
+
502
+ For each optional section that is ALREADY ACTIVE in forge.yaml (detected in Phase 1C):
503
+
504
+ **Default behavior (`PRESERVE=false`)**:
505
+ - Overwrite the section with newly detected values.
506
+ - Record the old values before overwriting so the Phase 5D summary can show "was X → now Y".
507
+ - No prompt. This is the expected behavior when re-running `/forgedock-init` to refresh config.
508
+
509
+ **`--preserve` flag (`PRESERVE=true`)**:
510
+ - Keep the existing section content unchanged. Skip generating new content for this section.
511
+ - Phase 5D summary will show: `(preserved — existing values kept)`
512
+
513
+ **`--interactive` flag (`INTERACTIVE=true`)**:
514
+ - Show the detected new value alongside the existing value, then ask:
515
+ ```
516
+ {section}: section already configured.
517
+ Current value: {EXISTING_VALUE}
518
+ Detected value: {DETECTED_VALUE}
519
+ Overwrite? [Y/n]
520
+ ```
521
+ - Default is Y (overwrite). If user says N, keep existing content.
522
+
523
+ **`--section <name>` targeted mode**: Always overwrite the targeted section regardless of `PRESERVE` flag — the explicit targeting signals user intent to update that section.
524
+
525
+ ### 5D: Write the file
526
+
527
+ Backup the existing file:
331
528
  ```bash
332
- # Search for health route definitions in server code
333
- grep -rn "health\|/ping\|/status" "$REPO_ROOT" \
334
- --include="*.py" --include="*.ts" --include="*.tsx" --include="*.go" --include="*.rs" --include="*.rb" \
335
- -l 2>/dev/null | head -10
336
-
337
- # Look for healthcheck patterns in compose/config files
338
- grep -rn "healthcheck\|HEALTH_URL\|health_endpoint\|/health" \
339
- "$REPO_ROOT" --include="*.yml" --include="*.yaml" --include="*.env*" \
340
- -l 2>/dev/null | head -5
529
+ cp "$FORGE_YAML" "${FORGE_YAML}.bak"
530
+ echo "Backed up: forge.yaml → forge.yaml.bak"
341
531
  ```
342
532
 
343
- From the results, infer the most likely health endpoint path:
344
- - If a route like `GET /health` or `GET /ping` is found → use that path
345
- - If a docker-compose healthcheck URL is found → extract the host:port+path
346
- - If docker-compose has a `services.api` block → infer `http://localhost:<port>/health`
533
+ Write the complete new file: required sections (preserved verbatim) + active optional sections (newly generated) + inactive optional sections (left as commented-out blocks matching forge.yaml.example structure).
347
534
 
348
- If an endpoint can be inferred:
349
- ```json
350
- { "value": "https://api.<detected-or-inferred-host>/<path>", "confidence": "medium", "source": "<file where found>", "why": "Health route found at <file>:<line>" }
351
- ```
535
+ Print a summary of what was written. For sections that were overwritten (existed before AND PRESERVE=false), show the old → new values for any field that changed. For sections that were newly added (did not exist before), show just the new values.
352
536
 
353
- If no endpoint found:
354
- ```json
355
- { "value": "", "confidence": "low", "source": "codebase scan", "why": "No health route detected; set this to the URL of your API's health endpoint" }
356
537
  ```
538
+ forge.yaml updated:
357
539
 
358
- ### 4B: Detect container names and service patterns
540
+ Required sections (preserved):
541
+ ✓ project
542
+ ✓ paths
543
+ ✓ branches
359
544
 
360
- ```bash
361
- for f in "$REPO_ROOT"/docker-compose*.yml; do
362
- [ -f "$f" ] || continue
363
- grep -E "^\s+container_name:" "$f" | head -20
364
- done
365
- ```
545
+ Optional sections (filled):
546
+ ✓ repos — 2 satellite(s) configured (new)
547
+ ✓ project_board — field IDs from project "Acme Board" (#1)
548
+ was: project_number: 2 → now: project_number: 1
549
+ ✓ review — tech stack updated
550
+ was: "Node.js" → now: "Node.js 20, TypeScript, PostgreSQL"
551
+ ✓ verification — health endpoint configured (new)
552
+ ✗ services — skipped (nothing detected)
366
553
 
367
- Extract the common prefix from all container names (e.g., `acme-api`, `acme-worker` → prefix `acme-`).
554
+ Optional sections (skipped — still commented out):
555
+ - services
368
556
 
369
- If container names are found:
370
- ```json
371
- { "value": "<prefix>", "confidence": "high", "source": "docker-compose.yml container_name entries", "why": "Containers named: <list>" }
557
+ Optional sections (preserved — --preserve flag):
558
+ - {section} — kept existing values unchanged
372
559
  ```
373
560
 
374
- If no docker-compose files found or no container_name entries:
375
- ```json
376
- { "value": "", "confidence": "low", "source": "codebase scan", "why": "No docker-compose container_name entries found; add your service name prefix here (e.g., 'acme-')" }
377
- ```
378
-
379
- ### 4C: Build verification enrichment
380
-
381
- ```json
382
- "verification": {
383
- "health_endpoint": { "value": "...", "confidence": "...", "source": "...", "why": "..." },
384
- "health_patterns": [
385
- { "value": "\"status\": \"ok\"", "confidence": "low", "source": "default", "why": "Standard ForgeDock health pattern; update if your API uses a different response shape" }
386
- ],
387
- "internal_service_patterns": [
388
- { "value": "<prefix>", "confidence": "...", "source": "...", "why": "..." }
389
- ]
390
- }
391
- ```
561
+ If nothing changed for an overwritten section (detected values match existing values), show `(no change)` instead of a diff.
392
562
 
393
563
  ---
394
564
 
395
- ## Phase 5: Assemble and Return Enriched ConfigDraft
396
-
397
- ### 5A: Merge enrichments into the draft
398
-
399
- Start from the incoming `DRAFT`. Merge the enriched sections:
400
- - `DRAFT.project_board` = Phase 1 output (or low-confidence placeholder)
401
- - `DRAFT.repos` = Phase 2 output (or low-confidence placeholder)
402
- - `DRAFT.review` = Phase 3 output
403
- - `DRAFT.verification` = Phase 4 output
565
+ ## Phase 6: Validation
404
566
 
405
- Never modify `DRAFT.project`, `DRAFT.paths`, `DRAFT.branches`, or `DRAFT.meta` — pass them through unchanged.
567
+ ### 6A: Repo access
406
568
 
407
- ### 5B: Add enrichment metadata
569
+ ```bash
570
+ gh repo view "${OWNER}/${REPO}" --json name,url 2>/dev/null \
571
+ || echo "WARNING: Cannot access ${OWNER}/${REPO} — check project.owner and project.repo"
572
+ ```
408
573
 
409
- Append to `DRAFT.meta`:
574
+ ### 6B: Path existence
410
575
 
411
- ```json
412
- {
413
- "enriched": true,
414
- "enrichment_backend": "skill",
415
- "enriched_at": "<ISO 8601 timestamp>",
416
- "enrichment_notes": ["<any skipped phases and reasons>"]
417
- }
576
+ ```bash
577
+ [ -d "$(grep 'root:' "$FORGE_YAML" | head -1 | awk '{print $2}' | tr -d '"')" ] \
578
+ && echo "✓ paths.root exists" \
579
+ || echo "WARNING: paths.root does not exist on this machine"
418
580
  ```
419
581
 
420
- ### 5C: Return the enriched ConfigDraft
582
+ ### 6C: Project board ID resolution
421
583
 
422
- Output the complete enriched ConfigDraft as a JSON blob on stdout. This is the skill's return value — the caller (CLI orchestrator or review-render) reads it from the output.
584
+ `project_board.project_id` must be a GitHub Projects v2 node ID with the `PVT_` prefix (e.g. `PVT_kwHOxxxxxxxxxxxxxxxx`). This value is returned by `gh project list --owner <owner> --format json | jq '.projects[] | .id'`. A manually-edited `forge.yaml` with an incorrect value (e.g. a project number instead of a node ID) will produce a confusing GraphQL error — the format check below catches this before attempting the API call.
423
585
 
424
- ```
425
- <ENRICHED_DRAFT_JSON>
586
+ ```bash
587
+ # Only run if project_board section was written
588
+ if grep -q "^project_board:" "$FORGE_YAML"; then
589
+ PROJECT_ID=$(grep "project_id:" "$FORGE_YAML" | head -1 | awk '{print $2}' | tr -d '"')
590
+ # Validate PVT_ prefix before GraphQL call — any other format will produce a confusing API error
591
+ if [[ "$PROJECT_ID" != PVT_* ]]; then
592
+ echo "ERROR: project_board.project_id must start with 'PVT_' (got: '$PROJECT_ID')"
593
+ echo " Expected format: PVT_kwHOxxxxxxxxxxxxxxxx"
594
+ echo " Obtain the correct value with: gh project list --owner ${OWNER} --format json | jq '.projects[] | .id'"
595
+ else
596
+ gh api graphql -f query='query { node(id: "'"$PROJECT_ID"'") { id __typename } }' 2>/dev/null \
597
+ | jq -e '.data.node.id' > /dev/null \
598
+ && echo "✓ project_board.project_id resolves" \
599
+ || echo "WARNING: project_board.project_id may be invalid — verify with: gh project list --owner ${OWNER}"
600
+ fi
601
+ fi
426
602
  ```
427
603
 
428
- **Output format requirements**:
429
- - Single JSON object, pretty-printed (2-space indent)
430
- - All sections present (project, paths, branches, project_board, repos, review, verification, meta)
431
- - Every leaf is `{ value, confidence, source, why }` — no raw strings
432
- - No trailing prose after the JSON blob
604
+ ### 6D: Satellite repo access
433
605
 
434
- ---
435
-
436
- ## Enrichment Summary (informational)
606
+ ```bash
607
+ # Only run if repos.satellites section was written
608
+ if grep -q "^repos:" "$FORGE_YAML"; then
609
+ # Extract satellite repo names and verify each is accessible
610
+ grep -A 20 "^repos:" "$FORGE_YAML" | grep "repo:" | tail -n +2 | while read -r line; do
611
+ SAT_REPO=$(echo "$line" | awk '{print $2}' | tr -d '"')
612
+ gh repo view "$SAT_REPO" --json name 2>/dev/null \
613
+ && echo "✓ satellite repo ${SAT_REPO} accessible" \
614
+ || echo "WARNING: satellite repo ${SAT_REPO} not accessible — check prefix and repo name"
615
+ done
616
+ fi
617
+ ```
437
618
 
438
- After the JSON output, print a human-readable enrichment summary for the operator's benefit. This is NOT part of the machine-readable output — it is separated by a `---` divider:
619
+ ### 6E: Final report
439
620
 
440
621
  ```
441
- ---
442
- ## Enrichment Summary
622
+ forge.yaml is ready.
443
623
 
444
- Sections enriched:
445
- project_board — <high|medium|low: brief note>
446
- repos — <N satellite candidates found | skipped>
447
- review — tech_stack=<value>, context from <source>
448
- verification — health_endpoint=<value | not found>, <N> container patterns
624
+ Validation results:
625
+ {validation output from 6A–6D}
449
626
 
450
- Skipped:
451
- <phase> — <reason>
627
+ Next steps:
628
+ 1. If forge.yaml contains sensitive paths (credentials file, local paths):
629
+ echo "forge.yaml" >> .gitignore
630
+ 2. Test the pipeline:
631
+ /work-on next
632
+ 3. If any WARNING appeared above, edit forge.yaml to fix the flagged values.
633
+ 4. Backup file: forge.yaml.bak (delete when satisfied)
452
634
 
453
- Low-confidence fields (will appear as TODO flags in forge.yaml):
454
- <list of field paths with low confidence and their why>
635
+ ForgeDock commands now use your project's config from forge.yaml.
455
636
  ```