forgedock 1.0.16 → 1.0.17
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 +21 -3
- package/bin/forgedock.mjs +1992 -540
- package/bin/github-app-manifest.json.license +2 -0
- package/bin/hooks/session-start.mjs +4 -5
- package/bin/registry.mjs +20 -0
- package/bin/tui.mjs +172 -96
- package/commands/adopt.md +433 -0
- package/commands/analytics.md +2 -0
- package/commands/audit-agents.md +2 -0
- package/commands/audit.md +2 -0
- package/commands/changelog.md +354 -0
- package/commands/cleanup.md +2 -0
- package/commands/compat-audit.md +330 -0
- package/commands/deploy-info.md +6 -5
- package/commands/diagnose.md +483 -0
- package/commands/explain.md +349 -0
- package/commands/forge-stats.md +2 -0
- package/commands/forgedock-init.md +2 -0
- package/commands/geo-audit.md +2 -0
- package/commands/incident-response.md +2 -0
- package/commands/issue.md +2 -0
- package/commands/milestone.md +2 -0
- package/commands/optimize.md +411 -0
- package/commands/orchestrate.md +118 -0
- package/commands/pipeline-health.md +60 -1
- package/commands/qa-sweep.md +2 -0
- package/commands/quality-gate.md +37 -0
- package/commands/replay.md +263 -0
- package/commands/resume.md +264 -0
- package/commands/review-pr-agents.md +5 -1
- package/commands/review-pr-staging.md +2 -0
- package/commands/review-pr.md +85 -3
- package/commands/rollback.md +2 -0
- package/commands/scope.md +280 -0
- package/commands/security-audit.md +2 -0
- package/commands/status.md +262 -0
- package/commands/sync-ecosystem.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/work-on/build/architect.md +128 -18
- package/commands/work-on/build/context.md +130 -17
- package/commands/work-on/build/implement.md +2 -0
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +32 -7
- package/commands/work-on/close.md +85 -0
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on/investigate.md +30 -3
- package/commands/work-on/review.md +13 -1
- package/commands/work-on-monolithic.md +2 -0
- package/commands/work-on.md +624 -36
- package/docs/BRAND.md +51 -0
- package/docs/CONFIG.md +94 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
- package/docs/claude-breakpoints.json +47 -0
- package/docs/site/getting-started.md +21 -0
- package/package.json +3 -3
- package/scripts/classify-lane.sh +145 -0
- package/scripts/transition-label.sh +161 -0
- package/scripts/validate-pr-target.sh +30 -0
- package/templates/devdocs/README.md +72 -0
- package/templates/devdocs/agent/using-forgedock.md +3 -0
- package/templates/devdocs/agent/using-github.md +3 -0
- package/templates/devdocs/index.yaml +162 -0
- package/templates/devdocs/project/architecture.md +3 -0
- package/templates/devdocs/project/conventions.md +3 -0
- package/templates/devdocs/project/custom-instructions.md +3 -0
- package/templates/devdocs/project/glossary.md +3 -0
- package/templates/devdocs/project/stack.md +3 -0
- package/templates/devdocs/templates/commit-convention.md +3 -0
- package/templates/devdocs/templates/issue.md +3 -0
- package/templates/devdocs/templates/pull-request.md +3 -0
- package/templates/devdocs/templates/review-finding.md +3 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Auto-generate release notes from merged PRs and FORGE:TRAJECTORY annotations — grouped by conventional commit type
|
|
3
|
+
argument-hint: [v1.0.0..v1.1.0 | since:YYYY-MM-DD | last-N]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /changelog — Release Notes Generator
|
|
7
|
+
|
|
8
|
+
**Input**: $ARGUMENTS (default: last 30 merged PRs on the staging branch)
|
|
9
|
+
|
|
10
|
+
Generate a structured markdown changelog from merged PRs and their FORGE:TRAJECTORY annotations. Groups entries by conventional commit type (feat, fix, refactor, docs, chore) and enriches each entry with task type and investigation verdict from the pipeline's annotation layer.
|
|
11
|
+
|
|
12
|
+
**Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
|
|
13
|
+
|
|
14
|
+
**NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Config Resolution
|
|
19
|
+
|
|
20
|
+
Read `forge.yaml` before running any phase. If missing, stop and tell the user to run `npx forgedock init`.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
24
|
+
|
|
25
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
26
|
+
echo "ERROR: forge.yaml not found. Run: npx forgedock init"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
31
|
+
GH_FLAG="-R $GH_REPO"
|
|
32
|
+
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
33
|
+
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
34
|
+
|
|
35
|
+
echo "Repo: $GH_REPO"
|
|
36
|
+
echo "Branch: $STAGING_BRANCH"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Phase 1: Parse Arguments
|
|
42
|
+
|
|
43
|
+
Resolve the date/commit range from `$ARGUMENTS`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ARGUMENTS="${ARGUMENTS:-}"
|
|
47
|
+
|
|
48
|
+
# Default: last 30 merged PRs
|
|
49
|
+
MODE="count"
|
|
50
|
+
COUNT=30
|
|
51
|
+
SINCE_DATE=""
|
|
52
|
+
TAG_FROM=""
|
|
53
|
+
TAG_TO=""
|
|
54
|
+
|
|
55
|
+
if [ -z "$ARGUMENTS" ]; then
|
|
56
|
+
MODE="count"
|
|
57
|
+
COUNT=30
|
|
58
|
+
echo "Mode: last $COUNT merged PRs (default)"
|
|
59
|
+
|
|
60
|
+
elif echo "$ARGUMENTS" | grep -qE '^last-[0-9]+$'; then
|
|
61
|
+
# last-N: last N merged PRs
|
|
62
|
+
COUNT=$(echo "$ARGUMENTS" | grep -oE '[0-9]+')
|
|
63
|
+
MODE="count"
|
|
64
|
+
echo "Mode: last $COUNT merged PRs"
|
|
65
|
+
|
|
66
|
+
elif echo "$ARGUMENTS" | grep -qE '^since:[0-9]{4}-[0-9]{2}-[0-9]{2}'; then
|
|
67
|
+
# since:YYYY-MM-DD: all PRs merged after this date
|
|
68
|
+
SINCE_DATE=$(echo "$ARGUMENTS" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')
|
|
69
|
+
MODE="since"
|
|
70
|
+
echo "Mode: since $SINCE_DATE"
|
|
71
|
+
|
|
72
|
+
elif echo "$ARGUMENTS" | grep -qE '^v?[A-Za-z0-9._-]+ *\.\. *v?[A-Za-z0-9._-]+'; then
|
|
73
|
+
# tag-to-tag: v1.0.0..v1.1.0
|
|
74
|
+
TAG_FROM=$(echo "$ARGUMENTS" | sed 's/ *\.\..*//')
|
|
75
|
+
TAG_TO=$(echo "$ARGUMENTS" | sed 's/.*\.\. *//')
|
|
76
|
+
MODE="tag-range"
|
|
77
|
+
echo "Mode: $TAG_FROM..$TAG_TO"
|
|
78
|
+
|
|
79
|
+
else
|
|
80
|
+
echo "WARNING: Unrecognized argument format: '$ARGUMENTS'"
|
|
81
|
+
echo "Falling back to: last 30 merged PRs"
|
|
82
|
+
MODE="count"
|
|
83
|
+
COUNT=30
|
|
84
|
+
fi
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Phase 2: Collect Merged PRs
|
|
90
|
+
|
|
91
|
+
Collect PRs merged to the staging branch in the resolved range. Use `while read` for all iteration — never `for VAR in $(command)`.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
echo ""
|
|
95
|
+
echo "=== Collecting merged PRs ==="
|
|
96
|
+
|
|
97
|
+
case "$MODE" in
|
|
98
|
+
count)
|
|
99
|
+
# Last N merged PRs on the staging branch
|
|
100
|
+
PR_JSON=$(gh pr list $GH_FLAG \
|
|
101
|
+
--state merged \
|
|
102
|
+
--base "$STAGING_BRANCH" \
|
|
103
|
+
--limit "$COUNT" \
|
|
104
|
+
--json number,title,mergedAt,body \
|
|
105
|
+
--jq 'sort_by(.mergedAt) | reverse | .[]')
|
|
106
|
+
;;
|
|
107
|
+
|
|
108
|
+
since)
|
|
109
|
+
# All merged PRs since SINCE_DATE
|
|
110
|
+
PR_JSON=$(gh pr list $GH_FLAG \
|
|
111
|
+
--state merged \
|
|
112
|
+
--base "$STAGING_BRANCH" \
|
|
113
|
+
--limit 200 \
|
|
114
|
+
--json number,title,mergedAt,body \
|
|
115
|
+
--jq "[.[] | select(.mergedAt >= \"${SINCE_DATE}T00:00:00Z\")] | sort_by(.mergedAt) | reverse | .[]")
|
|
116
|
+
;;
|
|
117
|
+
|
|
118
|
+
tag-range)
|
|
119
|
+
# PRs merged between two git tags — derive date window from tag commit dates
|
|
120
|
+
cd "$REPO_PATH"
|
|
121
|
+
|
|
122
|
+
if ! git rev-parse --verify "$TAG_FROM" >/dev/null 2>&1; then
|
|
123
|
+
echo "ERROR: Tag '$TAG_FROM' not found in git history."
|
|
124
|
+
echo "Available tags: $(git tag --sort=-version:refname | head -10 | tr '\n' ' ')"
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
if ! git rev-parse --verify "$TAG_TO" >/dev/null 2>&1; then
|
|
128
|
+
echo "ERROR: Tag '$TAG_TO' not found in git history."
|
|
129
|
+
echo "Available tags: $(git tag --sort=-version:refname | head -10 | tr '\n' ' ')"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
TAG_FROM_DATE=$(git log -1 --format="%aI" "$TAG_FROM")
|
|
134
|
+
TAG_TO_DATE=$(git log -1 --format="%aI" "$TAG_TO")
|
|
135
|
+
|
|
136
|
+
echo "Tag range: $TAG_FROM ($TAG_FROM_DATE) → $TAG_TO ($TAG_TO_DATE)"
|
|
137
|
+
|
|
138
|
+
PR_JSON=$(gh pr list $GH_FLAG \
|
|
139
|
+
--state merged \
|
|
140
|
+
--base "$STAGING_BRANCH" \
|
|
141
|
+
--limit 200 \
|
|
142
|
+
--json number,title,mergedAt,body \
|
|
143
|
+
--jq "[.[] | select(.mergedAt > \"$TAG_FROM_DATE\" and .mergedAt <= \"$TAG_TO_DATE\")] | sort_by(.mergedAt) | reverse | .[]")
|
|
144
|
+
;;
|
|
145
|
+
esac
|
|
146
|
+
|
|
147
|
+
# Empty-state guard: if no PRs matched the range, emit informative message and exit cleanly
|
|
148
|
+
if [ -z "$PR_JSON" ]; then
|
|
149
|
+
echo "No merged PRs found for this range."
|
|
150
|
+
exit 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Write PR list to a temp file so we can iterate without subshell word-splitting
|
|
154
|
+
TMPFILE=$(mktemp)
|
|
155
|
+
echo "$PR_JSON" > "$TMPFILE"
|
|
156
|
+
|
|
157
|
+
PR_COUNT=$(gh pr list $GH_FLAG --state merged --base "$STAGING_BRANCH" --limit "$COUNT" --json number --jq 'length' 2>/dev/null || echo "?")
|
|
158
|
+
echo "Collected PRs for changelog."
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Phase 3: Enrich with FORGE:TRAJECTORY Annotations
|
|
164
|
+
|
|
165
|
+
For each PR, extract the linked issue number and read its FORGE:TRAJECTORY comment.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
echo ""
|
|
169
|
+
echo "=== Reading FORGE:TRAJECTORY annotations ==="
|
|
170
|
+
|
|
171
|
+
# Accumulators — one line per group, tab-separated: PREFIX|PR_NUM|PR_TITLE|ISSUE_NUM|TASK_TYPE
|
|
172
|
+
declare -a GROUP_FEAT
|
|
173
|
+
declare -a GROUP_FIX
|
|
174
|
+
declare -a GROUP_REFACTOR
|
|
175
|
+
declare -a GROUP_DOCS
|
|
176
|
+
declare -a GROUP_CHORE
|
|
177
|
+
declare -a GROUP_OTHER
|
|
178
|
+
|
|
179
|
+
# Read each PR as a JSON object from the temp file using jq line-by-line
|
|
180
|
+
while IFS= read -r PR_OBJ; do
|
|
181
|
+
[ -z "$PR_OBJ" ] && continue
|
|
182
|
+
|
|
183
|
+
PR_NUM=$(echo "$PR_OBJ" | jq -r '.number')
|
|
184
|
+
PR_TITLE=$(echo "$PR_OBJ" | jq -r '.title')
|
|
185
|
+
PR_BODY=$(echo "$PR_OBJ" | jq -r '.body // ""')
|
|
186
|
+
PR_MERGED_AT=$(echo "$PR_OBJ" | jq -r '.mergedAt')
|
|
187
|
+
|
|
188
|
+
# Extract conventional commit prefix from title
|
|
189
|
+
# Matches: fix(scope): ..., feat: ..., refactor(scope): ..., etc.
|
|
190
|
+
PREFIX=$(echo "$PR_TITLE" | grep -oE '^(feat|fix|refactor|docs|chore|test|ci|build|perf|style)(\([^)]+\))?' | grep -oE '^[a-z]+' | head -1)
|
|
191
|
+
[ -z "$PREFIX" ] && PREFIX="other"
|
|
192
|
+
|
|
193
|
+
# Extract linked issue number from PR body (Closes #N, Fixes #N, Refs #N)
|
|
194
|
+
ISSUE_NUM=$(echo "$PR_BODY" | grep -oiE '(closes?|fixes?|resolves?|refs?)[[:space:]]+#[0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
195
|
+
|
|
196
|
+
# Read FORGE:TRAJECTORY from linked issue (when present)
|
|
197
|
+
TASK_TYPE=""
|
|
198
|
+
VERDICT=""
|
|
199
|
+
if [ -n "$ISSUE_NUM" ]; then
|
|
200
|
+
TRAJ=$(gh api "repos/$GH_REPO/issues/$ISSUE_NUM/comments" \
|
|
201
|
+
--jq '.[] | select(.body | contains("FORGE:TRAJECTORY")) | .body' 2>/dev/null | head -1)
|
|
202
|
+
|
|
203
|
+
if [ -n "$TRAJ" ]; then
|
|
204
|
+
# Extract task type from "Task type: Feature" in the Notes column
|
|
205
|
+
TASK_TYPE=$(echo "$TRAJ" | grep -oE 'Task type: [^|<]+' | sed 's/Task type: //' | tr -d ' ' | head -1)
|
|
206
|
+
# Extract verdict from "✅ CONFIRMED (HIGH)" or "✅ PARTIAL (MEDIUM)"
|
|
207
|
+
VERDICT=$(echo "$TRAJ" | grep -oE 'Investigation \| ✅ (CONFIRMED|PARTIAL|INVALID)' | grep -oE '(CONFIRMED|PARTIAL|INVALID)' | head -1)
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# Build entry line: "- prefix(scope): description (#PR_NUM) — closes #ISSUE_NUM [TASK_TYPE]"
|
|
212
|
+
SCOPE_PART=$(echo "$PR_TITLE" | grep -oE '^[a-zA-Z0-9_]+\([^)]+\)' | head -1)
|
|
213
|
+
if [ -n "$SCOPE_PART" ]; then
|
|
214
|
+
DESCRIPTION=$(echo "$PR_TITLE" | sed "s|^${SCOPE_PART}: *||")
|
|
215
|
+
else
|
|
216
|
+
DESCRIPTION=$(echo "$PR_TITLE" | sed 's/^[a-z]*: *//')
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
ENTRY="- ${PR_TITLE} ([#${PR_NUM}](https://github.com/${GH_REPO}/pull/${PR_NUM}))"
|
|
220
|
+
if [ -n "$ISSUE_NUM" ]; then
|
|
221
|
+
ENTRY="${ENTRY} — closes [#${ISSUE_NUM}](https://github.com/${GH_REPO}/issues/${ISSUE_NUM})"
|
|
222
|
+
fi
|
|
223
|
+
if [ -n "$TASK_TYPE" ]; then
|
|
224
|
+
ENTRY="${ENTRY} _(${TASK_TYPE})_"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Add to appropriate group
|
|
228
|
+
case "$PREFIX" in
|
|
229
|
+
feat) GROUP_FEAT+=("$ENTRY") ;;
|
|
230
|
+
fix) GROUP_FIX+=("$ENTRY") ;;
|
|
231
|
+
refactor) GROUP_REFACTOR+=("$ENTRY") ;;
|
|
232
|
+
docs) GROUP_DOCS+=("$ENTRY") ;;
|
|
233
|
+
chore|ci|build|test|perf|style) GROUP_CHORE+=("$ENTRY") ;;
|
|
234
|
+
*) GROUP_OTHER+=("$ENTRY") ;;
|
|
235
|
+
esac
|
|
236
|
+
|
|
237
|
+
done < <(cat "$TMPFILE" | jq -c '.')
|
|
238
|
+
|
|
239
|
+
rm -f "$TMPFILE"
|
|
240
|
+
echo "Annotation pass complete."
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Phase 4: Output Changelog
|
|
246
|
+
|
|
247
|
+
Render grouped markdown. Omit sections with no entries.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
echo ""
|
|
251
|
+
echo "========================================"
|
|
252
|
+
echo ""
|
|
253
|
+
|
|
254
|
+
# Determine header date range for display
|
|
255
|
+
case "$MODE" in
|
|
256
|
+
count) RANGE_LABEL="last ${COUNT} merged PRs" ;;
|
|
257
|
+
since) RANGE_LABEL="since ${SINCE_DATE}" ;;
|
|
258
|
+
tag-range) RANGE_LABEL="${TAG_FROM} → ${TAG_TO}" ;;
|
|
259
|
+
esac
|
|
260
|
+
|
|
261
|
+
TODAY=$(date +%Y-%m-%d)
|
|
262
|
+
echo "## Changelog — ${RANGE_LABEL} (generated ${TODAY})"
|
|
263
|
+
echo ""
|
|
264
|
+
echo "> Generated by [ForgeDock /changelog](https://github.com/${GH_REPO}) from merged PRs on \`${STAGING_BRANCH}\`"
|
|
265
|
+
echo ""
|
|
266
|
+
|
|
267
|
+
# Features
|
|
268
|
+
if [ ${#GROUP_FEAT[@]} -gt 0 ]; then
|
|
269
|
+
echo "### Features"
|
|
270
|
+
printf '%s\n' "${GROUP_FEAT[@]}"
|
|
271
|
+
echo ""
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Bug Fixes
|
|
275
|
+
if [ ${#GROUP_FIX[@]} -gt 0 ]; then
|
|
276
|
+
echo "### Bug Fixes"
|
|
277
|
+
printf '%s\n' "${GROUP_FIX[@]}"
|
|
278
|
+
echo ""
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Refactors
|
|
282
|
+
if [ ${#GROUP_REFACTOR[@]} -gt 0 ]; then
|
|
283
|
+
echo "### Refactors"
|
|
284
|
+
printf '%s\n' "${GROUP_REFACTOR[@]}"
|
|
285
|
+
echo ""
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# Documentation
|
|
289
|
+
if [ ${#GROUP_DOCS[@]} -gt 0 ]; then
|
|
290
|
+
echo "### Documentation"
|
|
291
|
+
printf '%s\n' "${GROUP_DOCS[@]}"
|
|
292
|
+
echo ""
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# Maintenance (chore, ci, build, test, perf, style)
|
|
296
|
+
if [ ${#GROUP_CHORE[@]} -gt 0 ]; then
|
|
297
|
+
echo "### Maintenance"
|
|
298
|
+
printf '%s\n' "${GROUP_CHORE[@]}"
|
|
299
|
+
echo ""
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Uncategorized (no conventional commit prefix)
|
|
303
|
+
if [ ${#GROUP_OTHER[@]} -gt 0 ]; then
|
|
304
|
+
echo "### Other"
|
|
305
|
+
printf '%s\n' "${GROUP_OTHER[@]}"
|
|
306
|
+
echo ""
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Summary line
|
|
310
|
+
TOTAL_PRS=$(( ${#GROUP_FEAT[@]} + ${#GROUP_FIX[@]} + ${#GROUP_REFACTOR[@]} + ${#GROUP_DOCS[@]} + ${#GROUP_CHORE[@]} + ${#GROUP_OTHER[@]} ))
|
|
311
|
+
echo "---"
|
|
312
|
+
echo ""
|
|
313
|
+
echo "**${TOTAL_PRS} changes** across: ${#GROUP_FEAT[@]} features, ${#GROUP_FIX[@]} fixes, ${#GROUP_REFACTOR[@]} refactors, ${#GROUP_DOCS[@]} docs, ${#GROUP_CHORE[@]} maintenance, ${#GROUP_OTHER[@]} other."
|
|
314
|
+
echo ""
|
|
315
|
+
echo "========================================"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Error Handling
|
|
321
|
+
|
|
322
|
+
- **forge.yaml missing**: Stop immediately with message to run `npx forgedock init`.
|
|
323
|
+
- **Tag not found**: List available tags, exit with non-zero status.
|
|
324
|
+
- **No PRs in range**: Emit "No merged PRs found for this range." and exit cleanly.
|
|
325
|
+
- **Issue has no FORGE:TRAJECTORY**: Skip gracefully — use PR title and number only. Do NOT crash.
|
|
326
|
+
- **gh CLI not authenticated**: `gh auth status` and `gh auth login` to fix.
|
|
327
|
+
- **Large date ranges**: Default limit is 200 PRs for `since:` mode — adjust `--limit` if more are expected.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Usage Examples
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# Default: last 30 merged PRs
|
|
335
|
+
/changelog
|
|
336
|
+
|
|
337
|
+
# Last 50 merged PRs
|
|
338
|
+
/changelog last-50
|
|
339
|
+
|
|
340
|
+
# All PRs merged since a specific date
|
|
341
|
+
/changelog since:2024-06-01
|
|
342
|
+
|
|
343
|
+
# PRs between two release tags
|
|
344
|
+
/changelog v1.0.0..v1.1.0
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Output Notes
|
|
348
|
+
|
|
349
|
+
The changelog is printed to stdout. Pipe or copy-paste into:
|
|
350
|
+
- A `CHANGELOG.md` file
|
|
351
|
+
- A GitHub release body (`gh release create v1.x.x --notes "..."`)
|
|
352
|
+
- A PR description for a staging → main deploy
|
|
353
|
+
|
|
354
|
+
FORGE:TRAJECTORY annotations are read on a best-effort basis. Issues closed outside the ForgeDock pipeline (no trajectory comment) will appear with PR title and number only, without the task type annotation.
|
package/commands/cleanup.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
description: Sweep closed issues for stale labels, missing workflow state, and Project board gaps — plus prune worktrees, branches, and milestones
|
|
3
3
|
argument-hint: [labels | branches | milestones | board | orphans | all]
|
|
4
4
|
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
5
7
|
|
|
6
8
|
# /cleanup — Full Hygiene Sweep
|
|
7
9
|
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: On-demand Claude Code compatibility and feature parity report — compares installed version against the breakpoints registry and lists affected ForgeDock features
|
|
3
|
+
argument-hint: [--refresh]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /compat-audit — Claude Code Compatibility Report
|
|
7
|
+
|
|
8
|
+
**Input**: $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
You are the ForgeDock compatibility auditor. Produce a point-in-time advisory report that shows whether the user's installed Claude Code version is current and which ForgeDock features may behave differently on their runtime. This command is **non-blocking** — it never aborts the session. All output is advisory.
|
|
11
|
+
|
|
12
|
+
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
|
|
13
|
+
**NEVER use plan mode (EnterPlanMode).**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Config Preamble
|
|
18
|
+
|
|
19
|
+
Read `forge.yaml` to resolve the ForgeDock installation path:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
23
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
24
|
+
FORGE_HOME_PATH=$(yq '.paths.root // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Fallback: resolve from this command file's own location if yq is unavailable
|
|
28
|
+
# or forge.yaml is absent. The commands/ directory is always inside FORGE_HOME.
|
|
29
|
+
if [ -z "$FORGE_HOME_PATH" ] || [ ! -d "$FORGE_HOME_PATH" ]; then
|
|
30
|
+
FORGE_HOME_PATH="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)"
|
|
31
|
+
fi
|
|
32
|
+
export FORGE_HOME_PATH
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 1: Parse Arguments
|
|
38
|
+
|
|
39
|
+
Parse `$ARGUMENTS` for the `--refresh` flag:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
REFRESH=false
|
|
43
|
+
for arg in $ARGUMENTS; do
|
|
44
|
+
[ "$arg" = "--refresh" ] && REFRESH=true
|
|
45
|
+
done
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**If `--refresh` is set**: delete the version cache before detection so the npm registry is queried for the latest version regardless of TTL:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
VERSION_CACHE="$HOME/.claude/forgedock/version-cache.json"
|
|
52
|
+
if [ "$REFRESH" = "true" ] && [ -f "$VERSION_CACHE" ]; then
|
|
53
|
+
rm -f "$VERSION_CACHE"
|
|
54
|
+
echo "Version cache cleared — will query npm registry for fresh data."
|
|
55
|
+
fi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Phase 2: Detect Installed Claude Code Version
|
|
61
|
+
|
|
62
|
+
Run `detectClaudeVersion()` from `bin/forge-utils.mjs`. This function reads from the cache at `~/.claude/forgedock/version-cache.json` (24h TTL) on a cache hit, or queries `npm info @anthropic-ai/claude-code version` and `claude --version` on a miss. It is fail-open — it never throws.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
VERSION_CACHE="$HOME/.claude/forgedock/version-cache.json"
|
|
66
|
+
INSTALLED="unknown"
|
|
67
|
+
LATEST="unknown"
|
|
68
|
+
STALE=false
|
|
69
|
+
DELTA=""
|
|
70
|
+
CACHED_AT=""
|
|
71
|
+
DAYS_SINCE_REFRESH="unknown"
|
|
72
|
+
|
|
73
|
+
if [ -f "$VERSION_CACHE" ]; then
|
|
74
|
+
INSTALLED=$(CACHE_PATH="$VERSION_CACHE" node -e "
|
|
75
|
+
try {
|
|
76
|
+
const c = JSON.parse(require('fs').readFileSync(process.env.CACHE_PATH, 'utf8'));
|
|
77
|
+
process.stdout.write(c.installed || 'unknown');
|
|
78
|
+
} catch { process.stdout.write('unknown'); }
|
|
79
|
+
" 2>/dev/null || echo "unknown")
|
|
80
|
+
|
|
81
|
+
LATEST=$(CACHE_PATH="$VERSION_CACHE" node -e "
|
|
82
|
+
try {
|
|
83
|
+
const c = JSON.parse(require('fs').readFileSync(process.env.CACHE_PATH, 'utf8'));
|
|
84
|
+
process.stdout.write(c.latest || 'unknown');
|
|
85
|
+
} catch { process.stdout.write('unknown'); }
|
|
86
|
+
" 2>/dev/null || echo "unknown")
|
|
87
|
+
|
|
88
|
+
STALE=$(CACHE_PATH="$VERSION_CACHE" node -e "
|
|
89
|
+
try {
|
|
90
|
+
const c = JSON.parse(require('fs').readFileSync(process.env.CACHE_PATH, 'utf8'));
|
|
91
|
+
process.stdout.write(c.stale === true ? 'true' : 'false');
|
|
92
|
+
} catch { process.stdout.write('false'); }
|
|
93
|
+
" 2>/dev/null || echo "false")
|
|
94
|
+
|
|
95
|
+
DELTA=$(CACHE_PATH="$VERSION_CACHE" node -e "
|
|
96
|
+
try {
|
|
97
|
+
const c = JSON.parse(require('fs').readFileSync(process.env.CACHE_PATH, 'utf8'));
|
|
98
|
+
process.stdout.write(c.delta || '');
|
|
99
|
+
} catch { process.stdout.write(''); }
|
|
100
|
+
" 2>/dev/null || echo "")
|
|
101
|
+
|
|
102
|
+
DAYS_SINCE_REFRESH=$(CACHE_PATH="$VERSION_CACHE" node -e "
|
|
103
|
+
try {
|
|
104
|
+
const c = JSON.parse(require('fs').readFileSync(process.env.CACHE_PATH, 'utf8'));
|
|
105
|
+
if (typeof c.cachedAt === 'number') {
|
|
106
|
+
const days = Math.floor((Date.now() - c.cachedAt) / 86400000);
|
|
107
|
+
process.stdout.write(days.toString());
|
|
108
|
+
} else {
|
|
109
|
+
process.stdout.write('unknown');
|
|
110
|
+
}
|
|
111
|
+
} catch { process.stdout.write('unknown'); }
|
|
112
|
+
" 2>/dev/null || echo "unknown")
|
|
113
|
+
else
|
|
114
|
+
# Cache absent — attempt a fresh detection by running detectClaudeVersion()
|
|
115
|
+
DETECT_RESULT=$(node --input-type=module <<'NODE_EOF' 2>/dev/null
|
|
116
|
+
import { pathToFileURL } from 'url';
|
|
117
|
+
import { join, resolve, dirname } from 'path';
|
|
118
|
+
import { fileURLToPath } from 'url';
|
|
119
|
+
const forgeHome = process.env.FORGE_HOME_PATH || '.';
|
|
120
|
+
const utilsPath = join(forgeHome, 'bin', 'forge-utils.mjs');
|
|
121
|
+
try {
|
|
122
|
+
const { detectClaudeVersion } = await import(pathToFileURL(utilsPath).href);
|
|
123
|
+
const r = await detectClaudeVersion();
|
|
124
|
+
process.stdout.write(JSON.stringify(r));
|
|
125
|
+
} catch {
|
|
126
|
+
process.stdout.write(JSON.stringify({ version: 'unknown' }));
|
|
127
|
+
}
|
|
128
|
+
NODE_EOF
|
|
129
|
+
)
|
|
130
|
+
INSTALLED=$(echo "$DETECT_RESULT" | node -e "
|
|
131
|
+
try {
|
|
132
|
+
const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
133
|
+
process.stdout.write(r.installed || r.version || 'unknown');
|
|
134
|
+
} catch { process.stdout.write('unknown'); }
|
|
135
|
+
" 2>/dev/null || echo "unknown")
|
|
136
|
+
LATEST=$(echo "$DETECT_RESULT" | node -e "
|
|
137
|
+
try {
|
|
138
|
+
const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
139
|
+
process.stdout.write(r.latest || 'unknown');
|
|
140
|
+
} catch { process.stdout.write('unknown'); }
|
|
141
|
+
" 2>/dev/null || echo "unknown")
|
|
142
|
+
fi
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Fail-open**: if all detection attempts return `unknown`, proceed to Phase 3 and report degraded output.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Phase 3: Load Breakpoints Registry
|
|
150
|
+
|
|
151
|
+
Read `docs/claude-breakpoints.json` from the ForgeDock installation root:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
BREAKPOINTS_FILE="${FORGE_HOME_PATH}/docs/claude-breakpoints.json"
|
|
155
|
+
BREAKPOINTS_JSON=""
|
|
156
|
+
|
|
157
|
+
if [ -f "$BREAKPOINTS_FILE" ]; then
|
|
158
|
+
BREAKPOINTS_JSON=$(cat "$BREAKPOINTS_FILE" 2>/dev/null || echo "")
|
|
159
|
+
else
|
|
160
|
+
echo "NOTE: docs/claude-breakpoints.json not found at ${FORGE_HOME_PATH}/docs/."
|
|
161
|
+
echo "This file is included in ForgeDock v1.x+. Run: npx forgedock update"
|
|
162
|
+
fi
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Phase 4: Diff — Identify Active Breakpoints
|
|
168
|
+
|
|
169
|
+
Compare each breakpoint's `version` against `INSTALLED` using **numeric** semver comparison (split on `.`, compare each segment as an integer — NOT lexicographic):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
if [ -n "$BREAKPOINTS_JSON" ] && [ "$INSTALLED" != "unknown" ]; then
|
|
173
|
+
ACTIVE_BREAKPOINTS=$(node -e "
|
|
174
|
+
try {
|
|
175
|
+
const data = JSON.parse(process.argv[1]);
|
|
176
|
+
const installed = process.argv[2];
|
|
177
|
+
|
|
178
|
+
function parseVer(v) {
|
|
179
|
+
return v.split('.').map(s => parseInt(s, 10));
|
|
180
|
+
}
|
|
181
|
+
function isLessThan(a, b) {
|
|
182
|
+
const pa = parseVer(a), pb = parseVer(b);
|
|
183
|
+
const len = Math.max(pa.length, pb.length);
|
|
184
|
+
for (let i = 0; i < len; i++) {
|
|
185
|
+
const ai = pa[i] || 0, bi = pb[i] || 0;
|
|
186
|
+
if (ai < bi) return true;
|
|
187
|
+
if (ai > bi) return false;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const breakpoints = data.breakpoints || [];
|
|
193
|
+
const active = breakpoints.filter(bp => isLessThan(installed, bp.version));
|
|
194
|
+
process.stdout.write(JSON.stringify(active));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
process.stdout.write('[]');
|
|
197
|
+
}
|
|
198
|
+
" -- "$BREAKPOINTS_JSON" "$INSTALLED" 2>/dev/null || echo "[]")
|
|
199
|
+
else
|
|
200
|
+
# Version unknown — cannot determine active breakpoints; list all as unknown status
|
|
201
|
+
ACTIVE_BREAKPOINTS="UNKNOWN"
|
|
202
|
+
fi
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Phase 5: Produce Report
|
|
208
|
+
|
|
209
|
+
Print the compat-audit report to stdout. This output is **advisory only** — it does not block the session or exit non-zero.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### Compat-Audit Report Format
|
|
214
|
+
|
|
215
|
+
Compose and print the following report, substituting resolved values:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
╔══════════════════════════════════════════════════════╗
|
|
219
|
+
║ ForgeDock / Claude Code Compat Audit ║
|
|
220
|
+
╚══════════════════════════════════════════════════════╝
|
|
221
|
+
|
|
222
|
+
Version Status
|
|
223
|
+
──────────────
|
|
224
|
+
Installed : {INSTALLED} (or "unknown — run /compat-audit --refresh")
|
|
225
|
+
Latest : {LATEST} (or "unknown — npm registry unavailable")
|
|
226
|
+
Status : {UP TO DATE | STALE — {DELTA}}
|
|
227
|
+
Cache age : {DAYS_SINCE_REFRESH} day(s) (refresh with: /compat-audit --refresh)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**If STALE is true** (installed differs from latest):
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
⚠ Your Claude Code is outdated.
|
|
234
|
+
Upgrade: npm update -g @anthropic-ai/claude-code
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**If STALE is false and both versions are known**:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
✓ Claude Code is up to date.
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**If versions are unknown**:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
? Version unknown. Is Claude Code installed? Try: claude --version
|
|
247
|
+
For a fresh check: /compat-audit --refresh
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Active Breakpoints Section
|
|
253
|
+
|
|
254
|
+
**Case A — installed version is known and breakpoints loaded**:
|
|
255
|
+
|
|
256
|
+
If `ACTIVE_BREAKPOINTS` is an empty array (`[]`):
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
Breakpoints
|
|
260
|
+
───────────
|
|
261
|
+
✓ No active breakpoints — your installed version meets all ForgeDock requirements.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If `ACTIVE_BREAKPOINTS` contains entries, print a table:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
Breakpoints (active — features introduced AFTER your installed version)
|
|
268
|
+
───────────────────────────────────────────────────────────────────────
|
|
269
|
+
Version │ Type │ Severity │ ForgeDock Impact
|
|
270
|
+
─────────┼──────────────────┼──────────┼──────────────────────────────
|
|
271
|
+
{version}│ {type} │ {severity}│ {forgedock_impact}
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
Affected ForgeDock features:
|
|
275
|
+
• {forgedock_impact for each active breakpoint}
|
|
276
|
+
|
|
277
|
+
Recommendation: upgrade to Claude Code {LATEST} to restore full ForgeDock functionality.
|
|
278
|
+
Command: npm update -g @anthropic-ai/claude-code
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Case B — version unknown but breakpoints loaded**:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
Breakpoints
|
|
285
|
+
───────────
|
|
286
|
+
? Version unknown — cannot determine which breakpoints are active.
|
|
287
|
+
All known breakpoints listed below for reference:
|
|
288
|
+
|
|
289
|
+
Version │ Type │ Severity │ Description
|
|
290
|
+
─────────┼──────────────────┼──────────┼──────────────────────────────
|
|
291
|
+
{version}│ {type} │ {severity}│ {description}
|
|
292
|
+
...
|
|
293
|
+
|
|
294
|
+
To get version-specific breakpoint status: /compat-audit --refresh
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Case C — breakpoints file not found**:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
Breakpoints
|
|
301
|
+
───────────
|
|
302
|
+
? Breakpoints registry not found (docs/claude-breakpoints.json).
|
|
303
|
+
Run: npx forgedock update # to get latest ForgeDock commands (includes breakpoints)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### Footer
|
|
309
|
+
|
|
310
|
+
Always end with:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
──────────────────────────────────────────────────────
|
|
314
|
+
This report is advisory. ForgeDock continues to work regardless of version gaps.
|
|
315
|
+
For the full ForgeDock version intelligence picture, also run: forgedock doctor
|
|
316
|
+
──────────────────────────────────────────────────────
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Error Handling
|
|
322
|
+
|
|
323
|
+
- **Cache unreadable** → treat as cache absent, attempt fresh detection
|
|
324
|
+
- **`claude --version` unavailable** → `INSTALLED = "unknown"`, continue
|
|
325
|
+
- **npm registry unavailable** → `LATEST = "unknown"`, continue
|
|
326
|
+
- **Breakpoints file missing** → print Case C above, continue
|
|
327
|
+
- **node unavailable** → print raw cache content if possible, otherwise print "version detection requires Node.js ≥ 18"
|
|
328
|
+
- **Any uncaught exception** → print "compat-audit encountered an error: {message}" and exit 0
|
|
329
|
+
|
|
330
|
+
The command MUST exit 0 in all cases. It NEVER blocks the Claude Code session.
|