forgedock 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/forge-utils.mjs +164 -0
- package/bin/forgedock.mjs +1914 -394
- package/bin/hooks/session-start.mjs +317 -0
- package/bin/init-detect.mjs +325 -0
- package/bin/init-enrich-api.mjs +211 -0
- package/bin/registry.mjs +358 -0
- package/bin/tui.mjs +470 -5
- package/commands/forgedock-init.md +305 -384
- package/commands/work-on.md +18 -0
- package/docs/CONFIG.md +185 -0
- package/docs/superpowers/specs/2026-06-09-intelligent-onboarding-design.md +172 -0
- package/package.json +5 -1
- package/scripts/verify-route-registration.sh +23 -17
|
@@ -1,534 +1,455 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
3
|
-
argument-hint:
|
|
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>
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# /forgedock-init —
|
|
6
|
+
# /forgedock-init — init-enrich Skill Backend
|
|
7
7
|
|
|
8
8
|
**Input**: $ARGUMENTS
|
|
9
9
|
|
|
10
|
-
You
|
|
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.
|
|
11
13
|
|
|
12
14
|
**Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
|
|
13
15
|
**NEVER use plan mode (EnterPlanMode).**
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
17
|
-
##
|
|
19
|
+
## ConfigDraft Contract
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|------|--------|
|
|
21
|
-
| (none) | Fill all optional sections not yet configured |
|
|
22
|
-
| `--force` | Overwrite ALL optional sections without asking |
|
|
23
|
-
| `--section <name>` | Fill only one section: `repos`, `project_board`, `services`, `review`, or `verification` |
|
|
21
|
+
Every leaf in a `ConfigDraft` has the shape:
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
FORCE = true if --force present
|
|
28
|
-
TARGET_SECTION = value from --section <name>, or "all"
|
|
23
|
+
```json
|
|
24
|
+
{ "value": "<string>", "confidence": "high|medium|low", "source": "<string>", "why": "<string>" }
|
|
29
25
|
```
|
|
30
26
|
|
|
31
|
-
|
|
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
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
ALLOWED_SECTIONS="repos project_board services review verification"
|
|
35
|
-
if [ "$TARGET_SECTION" != "all" ]; then
|
|
36
|
-
VALID=false
|
|
37
|
-
for s in $ALLOWED_SECTIONS; do
|
|
38
|
-
[ "$TARGET_SECTION" = "$s" ] && VALID=true && break
|
|
39
|
-
done
|
|
40
|
-
if [ "$VALID" = "false" ]; then
|
|
41
|
-
echo "Error: '$TARGET_SECTION' is not a valid section name."
|
|
42
|
-
echo ""
|
|
43
|
-
echo "Valid sections: repos, project_board, services, review, verification"
|
|
44
|
-
echo ""
|
|
45
|
-
echo "Usage: /forgedock-init --section <name>"
|
|
46
|
-
exit 1
|
|
47
|
-
fi
|
|
48
|
-
fi
|
|
49
|
-
```
|
|
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.
|
|
50
33
|
|
|
51
34
|
---
|
|
52
35
|
|
|
53
|
-
## Phase
|
|
36
|
+
## Phase 0: Parse Input and Extract Identity
|
|
54
37
|
|
|
55
|
-
###
|
|
38
|
+
### 0A: Parse ConfigDraft from $ARGUMENTS
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
40
|
+
`$ARGUMENTS` contains a ConfigDraft JSON string. Parse it:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
DRAFT = JSON.parse($ARGUMENTS)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If `$ARGUMENTS` is empty or not valid JSON, output an error and exit:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Error: /forgedock-init expects a ConfigDraft JSON string as its argument.
|
|
50
|
+
|
|
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)))"
|
|
54
|
+
|
|
55
|
+
Pass the output as the argument to /forgedock-init.
|
|
69
56
|
```
|
|
70
57
|
|
|
71
|
-
###
|
|
58
|
+
### 0B: Extract identity fields
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
Extract these from the incoming draft for use in all GitHub queries:
|
|
74
61
|
|
|
75
62
|
```
|
|
76
|
-
OWNER = project.owner
|
|
77
|
-
REPO = project.repo
|
|
78
|
-
REPO_ROOT = paths.root
|
|
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
|
|
79
66
|
```
|
|
80
67
|
|
|
81
|
-
|
|
68
|
+
Validate that OWNER and REPO are non-empty and not placeholder values before proceeding with any GitHub queries:
|
|
82
69
|
|
|
83
|
-
```
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
echo "Open forge.yaml and fill in the 'project:' section:"
|
|
88
|
-
echo " project:"
|
|
89
|
-
echo " owner: \"your-actual-github-org-or-username\""
|
|
90
|
-
echo ""
|
|
91
|
-
echo "Then re-run /forgedock-init."
|
|
92
|
-
exit 1
|
|
93
|
-
fi
|
|
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"
|
|
94
74
|
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
echo "Open forge.yaml and fill in the 'project:' section:"
|
|
99
|
-
echo " project:"
|
|
100
|
-
echo " repo: \"your-actual-repo-name\""
|
|
101
|
-
echo ""
|
|
102
|
-
echo "Then re-run /forgedock-init."
|
|
103
|
-
exit 1
|
|
104
|
-
fi
|
|
75
|
+
if REPO is empty or equals "your-repo-name":
|
|
76
|
+
set REPO = null
|
|
77
|
+
note: "project.repo is a placeholder — satellite enrichment skipped"
|
|
105
78
|
```
|
|
106
79
|
|
|
107
|
-
|
|
80
|
+
These are non-blocking — an incomplete draft still gets codebase enrichment (review, verification).
|
|
108
81
|
|
|
109
|
-
|
|
82
|
+
### 0C: Check GitHub auth
|
|
110
83
|
|
|
111
84
|
```bash
|
|
112
|
-
|
|
113
|
-
grep -q "^repos:" "$FORGE_YAML" && HAS_REPOS=true || HAS_REPOS=false
|
|
114
|
-
grep -q "^project_board:" "$FORGE_YAML" && HAS_PROJECT_BOARD=true || HAS_PROJECT_BOARD=false
|
|
115
|
-
grep -q "^services:" "$FORGE_YAML" && HAS_SERVICES=true || HAS_SERVICES=false
|
|
116
|
-
grep -q "^review:" "$FORGE_YAML" && HAS_REVIEW=true || HAS_REVIEW=false
|
|
117
|
-
grep -q "^verification:" "$FORGE_YAML" && HAS_VERIFICATION=true || HAS_VERIFICATION=false
|
|
85
|
+
gh auth status 2>&1
|
|
118
86
|
```
|
|
119
87
|
|
|
120
|
-
|
|
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.
|
|
89
|
+
|
|
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.
|
|
121
91
|
|
|
122
92
|
---
|
|
123
93
|
|
|
124
|
-
## Phase
|
|
94
|
+
## Phase 1: Enrich project_board
|
|
125
95
|
|
|
126
|
-
|
|
96
|
+
**Skip if**: `GH_AUTHED = false` or `OWNER = null`.
|
|
127
97
|
|
|
128
|
-
###
|
|
98
|
+
### 1A: Discover project boards
|
|
129
99
|
|
|
130
100
|
```bash
|
|
131
|
-
|
|
132
|
-
[
|
|
133
|
-
|
|
101
|
+
gh project list --owner "$OWNER" --format json 2>/dev/null \
|
|
102
|
+
| jq '[.projects[] | {number: .number, title: .title, id: .id}]'
|
|
103
|
+
```
|
|
134
104
|
|
|
135
|
-
|
|
136
|
-
[ -f "$REPO_ROOT/pyproject.toml" ] && grep -E "^\[project\]|^name|^requires-python|^dependencies" "$REPO_ROOT/pyproject.toml" | head -20
|
|
137
|
-
[ -f "$REPO_ROOT/requirements.txt" ] && head -20 "$REPO_ROOT/requirements.txt"
|
|
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).
|
|
138
106
|
|
|
139
|
-
|
|
140
|
-
|
|
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).
|
|
141
109
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
```
|
|
144
116
|
|
|
145
|
-
|
|
146
|
-
|
|
117
|
+
### 1B: Fetch field definitions
|
|
118
|
+
|
|
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 // [])}]'
|
|
147
122
|
```
|
|
148
123
|
|
|
149
|
-
|
|
124
|
+
If the query fails: skip Phase 1, emit low-confidence placeholder.
|
|
150
125
|
|
|
151
|
-
###
|
|
126
|
+
### 1C: Map fields to ForgeDock slots
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
# CLAUDE.md — primary context source
|
|
155
|
-
[ -f "$REPO_ROOT/CLAUDE.md" ] && cat "$REPO_ROOT/CLAUDE.md"
|
|
128
|
+
For each of the five ForgeDock field slots, search the field list case-insensitively:
|
|
156
129
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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" |
|
|
160
137
|
|
|
161
|
-
|
|
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`
|
|
162
141
|
|
|
163
|
-
###
|
|
142
|
+
### 1D: Map option IDs
|
|
164
143
|
|
|
165
|
-
|
|
166
|
-
# Container name prefixes for verification section
|
|
167
|
-
ls "$REPO_ROOT"/docker-compose*.yml 2>/dev/null
|
|
168
|
-
for f in "$REPO_ROOT"/docker-compose*.yml; do
|
|
169
|
-
[ -f "$f" ] || continue
|
|
170
|
-
grep -E "^\s+container_name:|^\s+image:" "$f" | head -20
|
|
171
|
-
done
|
|
172
|
-
```
|
|
144
|
+
For each single-select field that was successfully mapped, extract its option IDs.
|
|
173
145
|
|
|
174
|
-
|
|
146
|
+
For each of the required option slots, match case-insensitively:
|
|
175
147
|
|
|
176
|
-
|
|
148
|
+
**status options**: "Todo" / "To Do" → `todo`; "In Progress" / "In-Progress" → `in_progress`; "Done" / "Complete" → `done`
|
|
177
149
|
|
|
178
|
-
|
|
179
|
-
# Staging branch detection
|
|
180
|
-
git -C "$REPO_ROOT" branch -r | grep -E "staging|develop|dev" | head -5
|
|
150
|
+
**lane options**: "Fast" / "Fast Lane" → `fast`; "Feature" / "Feature Lane" → `feature`; "Sync" → `sync`
|
|
181
151
|
|
|
182
|
-
|
|
183
|
-
grep -rh "branches:" "$REPO_ROOT/.github/workflows/"*.yml 2>/dev/null | head -20
|
|
184
|
-
```
|
|
152
|
+
**priority options**: "P0" / "Critical" → `p0`; "P1" / "High" → `p1`; "P2" / "Medium" → `p2`; "P3" / "Low" → `p3`
|
|
185
153
|
|
|
186
|
-
|
|
154
|
+
**workflow options**: "Investigating" → `investigating`; "Building" → `building`; "In Review" / "In-Review" → `in_review`; "Merged" / "Complete" → `merged`
|
|
187
155
|
|
|
188
|
-
|
|
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`
|
|
189
159
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
160
|
+
### 1E: Build project_board enrichment
|
|
161
|
+
|
|
162
|
+
Assemble the enriched `project_board` section of the draft. If Phase 1 was skipped entirely:
|
|
163
|
+
|
|
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
|
+
}
|
|
194
172
|
```
|
|
195
173
|
|
|
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
|
+
|
|
196
176
|
---
|
|
197
177
|
|
|
198
|
-
## Phase
|
|
178
|
+
## Phase 2: Enrich repos.satellites
|
|
199
179
|
|
|
200
|
-
|
|
180
|
+
**Skip if**: `GH_AUTHED = false` or `OWNER = null` or `REPO = null`.
|
|
201
181
|
|
|
202
|
-
###
|
|
182
|
+
### 2A: List sibling repos
|
|
203
183
|
|
|
204
184
|
```bash
|
|
205
|
-
gh
|
|
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 // "")}]'
|
|
206
187
|
```
|
|
207
188
|
|
|
208
|
-
If
|
|
189
|
+
If the query fails or returns empty: skip Phase 2, emit low-confidence placeholder.
|
|
209
190
|
|
|
210
|
-
###
|
|
191
|
+
### 2B: Infer satellite candidates
|
|
211
192
|
|
|
212
|
-
|
|
213
|
-
# List projects owned by this org/user
|
|
214
|
-
gh project list --owner "$OWNER" --format json 2>/dev/null \
|
|
215
|
-
| jq '.projects[] | {number: .number, title: .title, id: .id}'
|
|
216
|
-
```
|
|
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):
|
|
217
194
|
|
|
218
|
-
|
|
219
|
-
- `
|
|
220
|
-
- `
|
|
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"
|
|
221
199
|
|
|
222
|
-
|
|
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)
|
|
223
203
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
```
|
|
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
|
|
229
208
|
|
|
230
|
-
|
|
231
|
-
- Look for fields named `Status`, `Lane`, `Component`, `Priority`, `Workflow` (case-insensitive)
|
|
232
|
-
- Record their `PVTSSF_...` IDs
|
|
209
|
+
### 2C: Build repos enrichment
|
|
233
210
|
|
|
234
|
-
|
|
235
|
-
# Get option IDs for single-select fields
|
|
236
|
-
gh project field-list "$PROJECT_NUMBER" --owner "$OWNER" --format json 2>/dev/null \
|
|
237
|
-
| jq '.fields[] | select(.type == "SINGLE_SELECT") | {name: .name, options: [.options[] | {name: .name, id: .id}]}'
|
|
238
|
-
```
|
|
211
|
+
For each satellite candidate:
|
|
239
212
|
|
|
240
|
-
|
|
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
|
+
}
|
|
219
|
+
```
|
|
241
220
|
|
|
242
|
-
|
|
221
|
+
If no candidates found:
|
|
243
222
|
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
}
|
|
248
228
|
```
|
|
249
229
|
|
|
250
|
-
Present the list and ask: "Do any of these serve as satellite repos that you'd route issues to with a prefix (e.g., `mcp:5`)?"
|
|
251
|
-
|
|
252
230
|
---
|
|
253
231
|
|
|
254
|
-
## Phase
|
|
232
|
+
## Phase 3: Enrich review
|
|
255
233
|
|
|
256
|
-
|
|
234
|
+
**Always runs** — does not require GitHub auth.
|
|
257
235
|
|
|
258
|
-
###
|
|
236
|
+
### 3A: Detect tech stack
|
|
259
237
|
|
|
260
|
-
|
|
238
|
+
For each supported ecosystem, check for its manifest file under `$REPO_ROOT`:
|
|
261
239
|
|
|
240
|
+
**Node.js / TypeScript**:
|
|
241
|
+
```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
|
|
262
251
|
```
|
|
263
|
-
The following repos were found under {OWNER}:
|
|
264
|
-
- {repo_name}: {description}
|
|
265
|
-
(...)
|
|
266
252
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
258
|
```
|
|
270
259
|
|
|
271
|
-
|
|
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
|
+
```
|
|
272
264
|
|
|
273
|
-
|
|
265
|
+
**Go**:
|
|
266
|
+
```bash
|
|
267
|
+
[ -f "$REPO_ROOT/go.mod" ] && head -10 "$REPO_ROOT/go.mod"
|
|
268
|
+
```
|
|
274
269
|
|
|
270
|
+
**Ruby**:
|
|
271
|
+
```bash
|
|
272
|
+
[ -f "$REPO_ROOT/Gemfile" ] && grep -E "rails|sinatra|roda|hanami|sidekiq" "$REPO_ROOT/Gemfile" | head -5
|
|
275
273
|
```
|
|
276
|
-
Found project board: "{TITLE}" (#{PROJECT_NUMBER})
|
|
277
274
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
275
|
+
**Docker / infrastructure**:
|
|
276
|
+
```bash
|
|
277
|
+
ls "$REPO_ROOT"/docker-compose*.yml 2>/dev/null | head -5
|
|
278
|
+
[ -f "$REPO_ROOT/Dockerfile" ] && head -5 "$REPO_ROOT/Dockerfile"
|
|
282
279
|
```
|
|
283
280
|
|
|
284
|
-
|
|
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 = ""`.
|
|
285
282
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
[2] Microsoft Clarity
|
|
290
|
-
[3] Google Analytics 4
|
|
291
|
-
[4] None / Skip this section
|
|
283
|
+
**Confidence**:
|
|
284
|
+
- One or more manifest files found → `medium` (detected from deps but not verified running)
|
|
285
|
+
- Nothing detected → `low`
|
|
292
286
|
|
|
293
|
-
|
|
294
|
-
```
|
|
287
|
+
### 3B: Extract project context
|
|
295
288
|
|
|
296
|
-
|
|
289
|
+
```bash
|
|
290
|
+
# Primary: CLAUDE.md
|
|
291
|
+
if [ -f "$REPO_ROOT/CLAUDE.md" ]; then
|
|
292
|
+
cat "$REPO_ROOT/CLAUDE.md"
|
|
293
|
+
fi
|
|
297
294
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
301
299
|
```
|
|
302
300
|
|
|
303
|
-
|
|
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)
|
|
304
306
|
|
|
305
|
-
|
|
306
|
-
Detected tech stack: {TECH_STACK}
|
|
307
|
+
If neither CLAUDE.md nor README.md exists: `REVIEW_CONTEXT = ""` at low confidence.
|
|
307
308
|
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
**Confidence**:
|
|
310
|
+
- Synthesized from CLAUDE.md → `high`
|
|
311
|
+
- Synthesized from README.md → `medium`
|
|
312
|
+
- No source found → `low`
|
|
310
313
|
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
314
321
|
```
|
|
315
322
|
|
|
316
|
-
|
|
323
|
+
---
|
|
317
324
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
## Phase 4: Enrich verification
|
|
326
|
+
|
|
327
|
+
**Always runs** — does not require GitHub auth.
|
|
328
|
+
|
|
329
|
+
### 4A: Detect health endpoint
|
|
330
|
+
|
|
331
|
+
```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
|
|
321
341
|
```
|
|
322
342
|
|
|
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`
|
|
347
|
+
|
|
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>" }
|
|
323
351
|
```
|
|
324
|
-
|
|
325
|
-
|
|
352
|
+
|
|
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" }
|
|
326
356
|
```
|
|
327
357
|
|
|
328
|
-
|
|
358
|
+
### 4B: Detect container names and service patterns
|
|
329
359
|
|
|
330
|
-
## Phase 5: Write forge.yaml
|
|
331
|
-
|
|
332
|
-
### 5A: Preserve required sections
|
|
333
|
-
|
|
334
|
-
Read the current `forge.yaml` content. Extract the complete `project:`, `paths:`, and `branches:` blocks verbatim — they must not change.
|
|
335
|
-
|
|
336
|
-
### 5B: Build optional section YAML
|
|
337
|
-
|
|
338
|
-
For each optional section the user confirmed:
|
|
339
|
-
|
|
340
|
-
**repos** (if satellites confirmed):
|
|
341
|
-
```yaml
|
|
342
|
-
repos:
|
|
343
|
-
default:
|
|
344
|
-
repo: "{OWNER}/{REPO}"
|
|
345
|
-
staging_branch: "{branches.staging}"
|
|
346
|
-
satellites:
|
|
347
|
-
- prefix: "{PREFIX}"
|
|
348
|
-
repo: "{OWNER}/{SATELLITE_REPO}"
|
|
349
|
-
staging_branch: "main"
|
|
350
|
-
local_path: "{REPO_ROOT}/../{SATELLITE_REPO}"
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
**project_board** (if board and field IDs found):
|
|
354
|
-
```yaml
|
|
355
|
-
project_board:
|
|
356
|
-
owner: "{OWNER}"
|
|
357
|
-
project_number: {PROJECT_NUMBER}
|
|
358
|
-
project_id: "{PROJECT_ID}" # Must be a PVT_... node ID — obtain via: gh project list --owner <owner> --format json | jq '.projects[] | .id'
|
|
359
|
-
field_ids:
|
|
360
|
-
status: "{STATUS_FIELD_ID}"
|
|
361
|
-
lane: "{LANE_FIELD_ID}"
|
|
362
|
-
component: "{COMPONENT_FIELD_ID}"
|
|
363
|
-
priority: "{PRIORITY_FIELD_ID}"
|
|
364
|
-
workflow: "{WORKFLOW_FIELD_ID}"
|
|
365
|
-
option_ids:
|
|
366
|
-
status:
|
|
367
|
-
todo: "{TODO_OPTION_ID}"
|
|
368
|
-
in_progress: "{IN_PROGRESS_OPTION_ID}"
|
|
369
|
-
done: "{DONE_OPTION_ID}"
|
|
370
|
-
lane:
|
|
371
|
-
fast: "{FAST_OPTION_ID}"
|
|
372
|
-
feature: "{FEATURE_OPTION_ID}"
|
|
373
|
-
priority:
|
|
374
|
-
p0: "{P0_OPTION_ID}"
|
|
375
|
-
p1: "{P1_OPTION_ID}"
|
|
376
|
-
p2: "{P2_OPTION_ID}"
|
|
377
|
-
p3: "{P3_OPTION_ID}"
|
|
378
|
-
workflow:
|
|
379
|
-
investigating: "{INVESTIGATING_OPTION_ID}"
|
|
380
|
-
building: "{BUILDING_OPTION_ID}"
|
|
381
|
-
in_review: "{IN_REVIEW_OPTION_ID}"
|
|
382
|
-
merged: "{MERGED_OPTION_ID}"
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
**services** (if analytics/monitoring confirmed):
|
|
386
|
-
```yaml
|
|
387
|
-
services:
|
|
388
|
-
domain: "{ROOT_DOMAIN}"
|
|
389
|
-
api_url: "{API_URL}"
|
|
390
|
-
analytics:
|
|
391
|
-
# Only include subsections the user confirmed
|
|
392
|
-
umami: # if selected
|
|
393
|
-
url: "{UMAMI_URL}"
|
|
394
|
-
website_id: "{UMAMI_WEBSITE_ID}"
|
|
395
|
-
clarity: # if selected
|
|
396
|
-
project_id: "{CLARITY_PROJECT_ID}"
|
|
397
|
-
ga4: # if selected
|
|
398
|
-
property_id: "{GA4_PROPERTY_ID}"
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
**review** (if tech stack or context provided):
|
|
402
|
-
```yaml
|
|
403
|
-
review:
|
|
404
|
-
tech_stack: "{TECH_STACK}"
|
|
405
|
-
context: |
|
|
406
|
-
{PROJECT_CONTEXT}
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
**verification** (if health endpoint or containers found):
|
|
410
|
-
```yaml
|
|
411
|
-
verification:
|
|
412
|
-
health_endpoint: "{HEALTH_ENDPOINT}"
|
|
413
|
-
health_patterns:
|
|
414
|
-
- '"status": "ok"'
|
|
415
|
-
internal_service_patterns:
|
|
416
|
-
- "{SERVICE_PREFIX}"
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### 5C: Overwrite protection
|
|
420
|
-
|
|
421
|
-
For each optional section that is ALREADY ACTIVE in forge.yaml (detected in Phase 1C):
|
|
422
|
-
|
|
423
|
-
If `FORCE=false`:
|
|
424
|
-
```
|
|
425
|
-
forge.yaml already has an active {section}: section.
|
|
426
|
-
Overwrite it with newly detected values? [y/N]
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
If user says N: keep existing content for that section.
|
|
430
|
-
|
|
431
|
-
### 5D: Write the file
|
|
432
|
-
|
|
433
|
-
Backup the existing file:
|
|
434
360
|
```bash
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
437
365
|
```
|
|
438
366
|
|
|
439
|
-
|
|
367
|
+
Extract the common prefix from all container names (e.g., `acme-api`, `acme-worker` → prefix `acme-`).
|
|
440
368
|
|
|
441
|
-
|
|
369
|
+
If container names are found:
|
|
370
|
+
```json
|
|
371
|
+
{ "value": "<prefix>", "confidence": "high", "source": "docker-compose.yml container_name entries", "why": "Containers named: <list>" }
|
|
442
372
|
```
|
|
443
|
-
forge.yaml updated:
|
|
444
373
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
```
|
|
449
378
|
|
|
450
|
-
|
|
451
|
-
✓ repos — 2 satellite(s) configured
|
|
452
|
-
✓ project_board — field IDs from project "Acme Board" (#1)
|
|
453
|
-
✗ services — skipped (no analytics services)
|
|
454
|
-
✓ review — tech stack + context
|
|
455
|
-
✓ verification — health endpoint configured
|
|
379
|
+
### 4C: Build verification enrichment
|
|
456
380
|
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
}
|
|
459
391
|
```
|
|
460
392
|
|
|
461
393
|
---
|
|
462
394
|
|
|
463
|
-
## Phase
|
|
395
|
+
## Phase 5: Assemble and Return Enriched ConfigDraft
|
|
464
396
|
|
|
465
|
-
###
|
|
397
|
+
### 5A: Merge enrichments into the draft
|
|
466
398
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
471
404
|
|
|
472
|
-
|
|
405
|
+
Never modify `DRAFT.project`, `DRAFT.paths`, `DRAFT.branches`, or `DRAFT.meta` — pass them through unchanged.
|
|
473
406
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
407
|
+
### 5B: Add enrichment metadata
|
|
408
|
+
|
|
409
|
+
Append to `DRAFT.meta`:
|
|
410
|
+
|
|
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
|
+
}
|
|
478
418
|
```
|
|
479
419
|
|
|
480
|
-
###
|
|
420
|
+
### 5C: Return the enriched ConfigDraft
|
|
481
421
|
|
|
482
|
-
|
|
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.
|
|
483
423
|
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
if grep -q "^project_board:" "$FORGE_YAML"; then
|
|
487
|
-
PROJECT_ID=$(grep "project_id:" "$FORGE_YAML" | head -1 | awk '{print $2}' | tr -d '"')
|
|
488
|
-
# Validate PVT_ prefix before GraphQL call — any other format will produce a confusing API error
|
|
489
|
-
if [[ "$PROJECT_ID" != PVT_* ]]; then
|
|
490
|
-
echo "ERROR: project_board.project_id must start with 'PVT_' (got: '$PROJECT_ID')"
|
|
491
|
-
echo " Expected format: PVT_kwHOxxxxxxxxxxxxxxxx"
|
|
492
|
-
echo " Obtain the correct value with: gh project list --owner ${OWNER} --format json | jq '.projects[] | .id'"
|
|
493
|
-
else
|
|
494
|
-
gh api graphql -f query='query { node(id: "'"$PROJECT_ID"'") { id __typename } }' 2>/dev/null \
|
|
495
|
-
| jq -e '.data.node.id' > /dev/null \
|
|
496
|
-
&& echo "✓ project_board.project_id resolves" \
|
|
497
|
-
|| echo "WARNING: project_board.project_id may be invalid — verify with: gh project list --owner ${OWNER}"
|
|
498
|
-
fi
|
|
499
|
-
fi
|
|
424
|
+
```
|
|
425
|
+
<ENRICHED_DRAFT_JSON>
|
|
500
426
|
```
|
|
501
427
|
|
|
502
|
-
|
|
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
|
|
503
433
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
# Extract satellite repo names and verify each is accessible
|
|
508
|
-
grep -A 20 "^repos:" "$FORGE_YAML" | grep "repo:" | tail -n +2 | while read -r line; do
|
|
509
|
-
SAT_REPO=$(echo "$line" | awk '{print $2}' | tr -d '"')
|
|
510
|
-
gh repo view "$SAT_REPO" --json name 2>/dev/null \
|
|
511
|
-
&& echo "✓ satellite repo ${SAT_REPO} accessible" \
|
|
512
|
-
|| echo "WARNING: satellite repo ${SAT_REPO} not accessible — check prefix and repo name"
|
|
513
|
-
done
|
|
514
|
-
fi
|
|
515
|
-
```
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Enrichment Summary (informational)
|
|
516
437
|
|
|
517
|
-
|
|
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:
|
|
518
439
|
|
|
519
440
|
```
|
|
520
|
-
|
|
441
|
+
---
|
|
442
|
+
## Enrichment Summary
|
|
521
443
|
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
524
449
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
echo "forge.yaml" >> .gitignore
|
|
528
|
-
2. Test the pipeline:
|
|
529
|
-
/work-on next
|
|
530
|
-
3. If any WARNING appeared above, edit forge.yaml to fix the flagged values.
|
|
531
|
-
4. Backup file: forge.yaml.bak (delete when satisfied)
|
|
450
|
+
Skipped:
|
|
451
|
+
<phase> — <reason>
|
|
532
452
|
|
|
533
|
-
|
|
453
|
+
Low-confidence fields (will appear as TODO flags in forge.yaml):
|
|
454
|
+
<list of field paths with low confidence and their why>
|
|
534
455
|
```
|