forgedock 1.0.21 → 1.0.22
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 +134 -80
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +320 -26
- package/commands/quality-gate.md +91 -49
- package/commands/review-pr-agents.md +62 -38
- package/commands/review-pr.md +33 -2
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +5 -1
- package/templates/forge.yaml.minimal +44 -0
|
@@ -125,6 +125,11 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
|
|
|
125
125
|
ISSUE_LABELS=$(gh issue view {NUMBER} -R {GH_REPO} --json labels \
|
|
126
126
|
--jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
|
|
127
127
|
echo "Issue labels: ${ISSUE_LABELS:-none}"
|
|
128
|
+
# Separate newline-joined list for the loop below — GitHub label names CAN
|
|
129
|
+
# contain spaces (e.g. "good first issue"), so the space-joined $ISSUE_LABELS
|
|
130
|
+
# above is only safe for display, never for iteration.
|
|
131
|
+
ISSUE_LABELS_LIST=$(gh issue view {NUMBER} -R {GH_REPO} --json labels \
|
|
132
|
+
--jq '.labels[].name' 2>/dev/null || echo "")
|
|
128
133
|
|
|
129
134
|
# Read index content
|
|
130
135
|
INDEX_CONTENT=$(cat "$INDEX_PATH" 2>/dev/null || echo "")
|
|
@@ -139,7 +144,12 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
|
|
|
139
144
|
|
|
140
145
|
# Extract domain blocks matching any issue label keyword
|
|
141
146
|
DOMAIN_PATHS=""
|
|
142
|
-
|
|
147
|
+
# $ISSUE_LABELS_LIST is one label per line — herestring (not a piped
|
|
148
|
+
# `| while read`, which would run the loop body in a subshell and discard
|
|
149
|
+
# DOMAIN_PATHS once the loop exits) preserves newline-safety for labels
|
|
150
|
+
# containing spaces.
|
|
151
|
+
while IFS= read -r label; do
|
|
152
|
+
[ -z "$label" ] && continue
|
|
143
153
|
# Strip workflow:, priority:, review-finding prefixes — use bare keyword
|
|
144
154
|
KEYWORD=$(echo "$label" | sed 's/^workflow://; s/^priority://; s/^review-finding$//')
|
|
145
155
|
[ -z "$KEYWORD" ] && continue
|
|
@@ -158,7 +168,7 @@ if [ -n "$DEVDOCS_PATH" ] && [ -f "$INDEX_PATH" ]; then
|
|
|
158
168
|
echo "Domain '${SAFE_KEYWORD}' matched — adding docs: $(echo "$BLOCK_PATHS" | tr '\n' ' ')"
|
|
159
169
|
DOMAIN_PATHS="${DOMAIN_PATHS}${BLOCK_PATHS}"$'\n'
|
|
160
170
|
fi
|
|
161
|
-
done
|
|
171
|
+
done <<< "$ISSUE_LABELS_LIST"
|
|
162
172
|
|
|
163
173
|
# Combine always_load + domain paths (deduplicate)
|
|
164
174
|
ALL_PATHS=$(printf "%s\n%s" "$ALWAYS_LOAD_PATHS" "$DOMAIN_PATHS" \
|
|
@@ -397,7 +407,12 @@ If `GIST_SUMMARIES` is non-empty, it will be included in the `### Prior Investig
|
|
|
397
407
|
Query closed issues with `review-finding` label, searching by filename:
|
|
398
408
|
|
|
399
409
|
```bash
|
|
400
|
-
|
|
410
|
+
# {AFFECTED_FILES} is a space-separated file path list (see contract note above)
|
|
411
|
+
# — split explicitly on IFS=' ' into an array instead of a bare
|
|
412
|
+
# `for file in {AFFECTED_FILES}`, which word-splits on the shell's default IFS
|
|
413
|
+
# (space, tab, AND newline) and would corrupt any path containing a space.
|
|
414
|
+
IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
|
|
415
|
+
for file in "${AFFECTED_FILES_ARR[@]}"; do
|
|
401
416
|
basename=$(basename "$file" .py)
|
|
402
417
|
gh issue list -R {GH_REPO} \
|
|
403
418
|
--state closed \
|
|
@@ -425,7 +440,7 @@ Keep findings where the filename or function name appears in the title or body.
|
|
|
425
440
|
|
|
426
441
|
## Phase C2: Past Bugs in the Same Module
|
|
427
442
|
|
|
428
|
-
Mine git log for commit messages referencing issues, then fetch those issues
|
|
443
|
+
Mine git log for commit messages referencing issues, then fetch those issues. Also read commit bodies/diffs directly — local git history is near-free relative to `gh api` round-trips, and commit bodies often explain the "why" without needing to fetch the linked issue at all.
|
|
429
444
|
|
|
430
445
|
```bash
|
|
431
446
|
# Step 1: find issue numbers from git history on affected files
|
|
@@ -444,6 +459,20 @@ Filter: keep only `bug`, `fix`, or `review-finding` labeled issues. Skip feature
|
|
|
444
459
|
|
|
445
460
|
**Max results**: 5 issues.
|
|
446
461
|
|
|
462
|
+
**Direct commit-body read (bounded, prefer over `gh api` when it already answers "why")**: For the top 5 commits touching `{AFFECTED_FILES}`, read the commit subject + body directly instead of round-tripping to `gh issue view` when the body already explains the change:
|
|
463
|
+
```bash
|
|
464
|
+
git log -5 --format='%h %ad %s%n%b' --date=short -- {AFFECTED_FILES}
|
|
465
|
+
```
|
|
466
|
+
If a commit body fully explains the prior bug/fix (common for squashed or fix-up commits with no `#NNN` reference), use it directly as a "Past Bug in This Module" entry — do not require a linked GitHub issue to exist.
|
|
467
|
+
|
|
468
|
+
**Pickaxe pass (has this exact area been fixed before?)** — one bounded pass, capped at 5 hits, keyed on the suspected symbol/string from the Builder Contract or investigation report:
|
|
469
|
+
```bash
|
|
470
|
+
git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
|
|
471
|
+
# Use -G instead of -S for regex patterns
|
|
472
|
+
git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
|
|
473
|
+
```
|
|
474
|
+
Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
|
|
475
|
+
|
|
447
476
|
---
|
|
448
477
|
|
|
449
478
|
## Phase C3: Related Code Paths
|
|
@@ -526,8 +555,10 @@ gh issue comment {NUMBER} -R {GH_REPO} --body "<!-- FORGE:CONTEXT -->
|
|
|
526
555
|
- #{NUM}: \"{TITLE}\" — root cause: {ROOT_CAUSE}
|
|
527
556
|
|
|
528
557
|
### Past Bugs in This Module
|
|
529
|
-
<!-- List of closed bug issues from git log mining
|
|
558
|
+
<!-- List of closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or
|
|
559
|
+
commit bodies read directly per the C2 direct-commit-body step). If none: 'No prior bugs found in git history.' -->
|
|
530
560
|
- #{NUM}: \"{TITLE}\" — root cause: {SNIPPET}
|
|
561
|
+
- {COMMIT_HASH} (no linked issue): {COMMIT_SUBJECT} — {WHY_RELEVANT, from commit body or pickaxe hit}
|
|
531
562
|
|
|
532
563
|
### Related Code Paths (must stay consistent)
|
|
533
564
|
<!-- Files that import or call the changed functions. Builder must read and validate these. -->
|
|
@@ -172,21 +172,30 @@ When changed files touch database engine configuration, flag for manual connecti
|
|
|
172
172
|
```bash
|
|
173
173
|
cd {WORKTREE_PATH}
|
|
174
174
|
DB_CONFIG_FILES=""
|
|
175
|
-
|
|
175
|
+
# Process substitution (< <(...)), NOT a piped `| while read`, so DB_CONFIG_FILES
|
|
176
|
+
# set inside the loop body survives past the loop (a piped while-read would run
|
|
177
|
+
# in a subshell and silently discard the accumulator).
|
|
178
|
+
while IFS= read -r f; do
|
|
179
|
+
[ -z "$f" ] && continue
|
|
176
180
|
grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
|
|
177
|
-
DB_CONFIG_FILES="$DB_CONFIG_FILES
|
|
178
|
-
done
|
|
181
|
+
DB_CONFIG_FILES="${DB_CONFIG_FILES}${f}"$'\n'
|
|
182
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
179
183
|
|
|
180
184
|
if [ -n "$DB_CONFIG_FILES" ]; then
|
|
181
|
-
echo "DB CONFIG CHANGE DETECTED in
|
|
185
|
+
echo "DB CONFIG CHANGE DETECTED in:"
|
|
186
|
+
echo "$DB_CONFIG_FILES"
|
|
182
187
|
echo "ACTION: Verify database connectivity after deploy — changes to engine config, connect_args, or session factories can cause silent runtime failures."
|
|
183
188
|
echo "RECOMMENDED: Run a minimal connectivity test (e.g., SELECT 1) through the modified session/engine path."
|
|
184
189
|
|
|
185
190
|
# Check for lambda/callable in connect_args — the exact bug class from PR #14391
|
|
186
|
-
|
|
191
|
+
# $DB_CONFIG_FILES is one path per line (built above) — herestring, not a
|
|
192
|
+
# piped `| while read`, so behavior stays consistent with the other fixes
|
|
193
|
+
# in this sweep even though no accumulator is set inside this particular loop.
|
|
194
|
+
while IFS= read -r f; do
|
|
195
|
+
[ -z "$f" ] && continue
|
|
187
196
|
grep -nE "lambda.*:.*['\"]|=lambda" "$f" 2>/dev/null | grep -iE "connect_args|prepared_statement|pool|engine" && \
|
|
188
197
|
echo "WARNING: Lambda/callable in database configuration in $f — verify callback signature matches library's expected calling convention"
|
|
189
|
-
done
|
|
198
|
+
done <<< "$DB_CONFIG_FILES"
|
|
190
199
|
fi
|
|
191
200
|
```
|
|
192
201
|
|
|
@@ -234,7 +243,7 @@ After the gate passes, commit all staged changes in a single commit. This includ
|
|
|
234
243
|
```bash
|
|
235
244
|
cd {WORKTREE_PATH}
|
|
236
245
|
git add -u
|
|
237
|
-
git commit -m "fix({SCOPE}): {description} (#NUMBER)"
|
|
246
|
+
git commit -s -m "fix({SCOPE}): {description} (#NUMBER)"
|
|
238
247
|
```
|
|
239
248
|
|
|
240
249
|
Where `{SCOPE}` is the command or module scope from the contract (e.g. `work-on`, `quality-gate`), and `{description}` summarises the implementation. Use the commit convention from the contract:
|
|
@@ -279,6 +279,125 @@ Output to stdout (returned to calling agent):
|
|
|
279
279
|
|
|
280
280
|
---
|
|
281
281
|
|
|
282
|
+
## Phase C4.5: Pipeline Summary Card (MANDATORY)
|
|
283
|
+
|
|
284
|
+
The shareable artifact. After the close completes, render a box-drawing summary card to
|
|
285
|
+
stdout (terminal screenshot moment) AND compute a machine-readable twin that Phase C5
|
|
286
|
+
embeds in the `FORGE:TRAJECTORY` comment for platform consumption.
|
|
287
|
+
|
|
288
|
+
**All stats are real — pulled from `gh`/`git`. Every lookup degrades gracefully: a missing
|
|
289
|
+
value renders as `—` and NEVER aborts the card. Do NOT fabricate stats.**
|
|
290
|
+
|
|
291
|
+
### C4.5a: Gather real stats
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# Commit / diff stats from the merged PR (single API call). Fallbacks to "—" if absent.
|
|
295
|
+
PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
|
|
296
|
+
COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
|
|
297
|
+
ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
|
|
298
|
+
DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
|
|
299
|
+
PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
|
|
300
|
+
IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
|
|
301
|
+
|
|
302
|
+
# Review summary — count domain-agent verdicts posted by /review-pr on the PR.
|
|
303
|
+
REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
304
|
+
--jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
|
|
305
|
+
# NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
|
|
306
|
+
# `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
|
|
307
|
+
# and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
|
|
308
|
+
APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
|
|
309
|
+
CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
|
|
310
|
+
TOTAL_AGENTS=$((APPROVED + CHANGES))
|
|
311
|
+
# Blockers = review-finding issues created by this PR that are still open (best-effort).
|
|
312
|
+
BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
|
|
313
|
+
if [ "$TOTAL_AGENTS" -gt 0 ]; then
|
|
314
|
+
REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
|
|
315
|
+
else
|
|
316
|
+
REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# Elapsed wall-clock: first FORGE agent comment → now.
|
|
320
|
+
FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
321
|
+
--jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
|
|
322
|
+
if [ -n "$FIRST_TS" ]; then
|
|
323
|
+
START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
|
|
324
|
+
|| python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
|
|
325
|
+
|| echo "")
|
|
326
|
+
NOW_EPOCH=$(date -u +%s)
|
|
327
|
+
if [ -n "$START_EPOCH" ]; then
|
|
328
|
+
ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
|
|
329
|
+
ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
|
|
330
|
+
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
331
|
+
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
332
|
+
|
|
333
|
+
# Pipeline line + status — reflect the ACTUAL terminal state.
|
|
334
|
+
# merged → investigate → architect → build → review → merge ✓
|
|
335
|
+
# decomposed→ investigate → decompose ⏹ (sub-issues spawned)
|
|
336
|
+
# invalid → investigate → invalid ✗
|
|
337
|
+
# blocked → investigate → … → blocked ⚠ (needs-human)
|
|
338
|
+
# draft PR → append "(draft)" to the merge segment
|
|
339
|
+
case "{TERMINAL_STATE}" in
|
|
340
|
+
decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
|
|
341
|
+
invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
|
|
342
|
+
blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
|
|
343
|
+
*) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
|
|
344
|
+
esac
|
|
345
|
+
[ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### C4.5b: Render the card to stdout
|
|
349
|
+
|
|
350
|
+
Print the card to stdout (the calling agent surfaces it in the terminal). Card inner
|
|
351
|
+
width is **51** columns. Truncate the title with an ellipsis (`…`) if `#{NUMBER} — {TITLE}`
|
|
352
|
+
exceeds the field; pad shorter lines with spaces so the right border `║` stays aligned.
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
╔═══════════════════════════════════════════════════╗
|
|
356
|
+
║ ForgeDock Pipeline Complete ║
|
|
357
|
+
╠═══════════════════════════════════════════════════╣
|
|
358
|
+
║ ║
|
|
359
|
+
║ Issue: #{NUMBER} — {TITLE} ║
|
|
360
|
+
║ Pipeline: {PIPELINE_LINE} ║
|
|
361
|
+
║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
|
|
362
|
+
║ PR: #{PR_NUMBER} (merged to {PR_TARGET}) ║
|
|
363
|
+
║ Review: {REVIEW_SUMMARY} ║
|
|
364
|
+
║ Time: {ELAPSED} ║
|
|
365
|
+
║ ║
|
|
366
|
+
╚═══════════════════════════════════════════════════╝
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Edge-case rendering**:
|
|
370
|
+
- Decomposed: title line stays; `Pipeline:` shows `investigate → decompose ⏹`; `PR:`, `Review:`, `Commits:` render `—`; the header reads `ForgeDock Pipeline — Decomposed`.
|
|
371
|
+
- Invalid: header `ForgeDock Pipeline — Closed (invalid)`; `Pipeline:` shows `investigate → invalid ✗`; downstream stats `—`.
|
|
372
|
+
- Blocked / needs-human: header `ForgeDock Pipeline — Blocked`; `Review:`/`PR:` reflect last known state; remaining stats `—`.
|
|
373
|
+
- Draft PR: `PR:` line appends `(draft)` and the merge segment is not marked `✓`.
|
|
374
|
+
|
|
375
|
+
### C4.5c: Build the machine-readable twin
|
|
376
|
+
|
|
377
|
+
Assemble the JSON object below (used verbatim by Phase C5). Numeric stats that were `—`
|
|
378
|
+
become `null` in JSON; never emit `"—"` as a number.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
CARD_JSON=$(jq -nc \
|
|
382
|
+
--argjson issue {NUMBER} \
|
|
383
|
+
--arg title "{TITLE}" \
|
|
384
|
+
--arg status "$CARD_STATUS" \
|
|
385
|
+
--arg pipeline "$PIPELINE_LINE" \
|
|
386
|
+
--arg pr "{PR_NUMBER}" \
|
|
387
|
+
--arg target "$PR_TARGET" \
|
|
388
|
+
--arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
|
|
389
|
+
--arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
|
|
390
|
+
--argjson elapsed "${ELAPSED_SECS:-0}" \
|
|
391
|
+
'{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
|
|
392
|
+
pr:($pr|tonumber? // null), pr_target:$target,
|
|
393
|
+
commits:($commits|tonumber? // null),
|
|
394
|
+
additions:($adds|tonumber? // null),
|
|
395
|
+
deletions:($dels|tonumber? // null),
|
|
396
|
+
review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
282
401
|
## Phase C5: Trajectory Log (MANDATORY)
|
|
283
402
|
|
|
284
403
|
Post the `<!-- FORGE:TRAJECTORY -->` comment as the final pipeline record:
|
|
@@ -305,9 +424,18 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
|
|
|
305
424
|
|
|
306
425
|
**Anomalies**: None
|
|
307
426
|
|
|
308
|
-
**Pipeline completed**: {TIMESTAMP}
|
|
427
|
+
**Pipeline completed**: {TIMESTAMP}
|
|
428
|
+
|
|
429
|
+
<!-- FORGE:CARD ${CARD_JSON} -->"
|
|
309
430
|
```
|
|
310
431
|
|
|
432
|
+
The `<!-- FORGE:CARD {...} -->` block carries the machine-readable summary computed in
|
|
433
|
+
Phase C4.5c. It is wrapped in an HTML comment so it stays hidden in GitHub's rendered view
|
|
434
|
+
(keeping the trajectory comment clean) while remaining greppable in the raw body for platform
|
|
435
|
+
consumption — `/orchestrate` reads it to build per-issue cards. This block is **additive**:
|
|
436
|
+
all existing `FORGE:TRAJECTORY` consumers select via `contains("FORGE:TRAJECTORY")` and parse
|
|
437
|
+
the markdown table, so the embedded JSON does not affect them.
|
|
438
|
+
|
|
311
439
|
Where:
|
|
312
440
|
- `{PARENT_STATUS}` = `⏭ Skipped` (if no parent) or `✅ Complete` (if parent updated)
|
|
313
441
|
- `{PARENT_NOTES}` = `No parent tracker` or `Checked off in #{PARENT_REF}`
|
|
@@ -72,13 +72,30 @@ If the issue specifies a **Code branch** (`**Code branch**: \`{branch}\``), chec
|
|
|
72
72
|
1. **Check the right branch** — read from the branch specified in the issue body (`**Code branch**: \`{branch}\``) if present
|
|
73
73
|
2. **Read domain files** — start with the key files for the affected domain
|
|
74
74
|
3. **Verify claims** — does the code actually have the problem described?
|
|
75
|
-
4. **Git blame** — trace when/why the relevant code was written
|
|
75
|
+
4. **Git blame** — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
|
|
76
|
+
```bash
|
|
77
|
+
# Introducing commit for each affected file (first commit that added it)
|
|
78
|
+
git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
|
|
79
|
+
# Last-touch commit (most recent change)
|
|
80
|
+
git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
|
|
81
|
+
# Line-level blame for a specific suspect hunk, if the issue names one
|
|
82
|
+
git blame -L {start},{end} -- {affected_file}
|
|
83
|
+
```
|
|
84
|
+
Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
|
|
76
85
|
5. **Domain context discovery** (narrow scope only, 1–5 files):
|
|
77
86
|
```bash
|
|
78
87
|
git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u
|
|
79
88
|
gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
|
|
80
89
|
```
|
|
81
90
|
Keep only file/function-level overlap. Max 5 related issues. Everything is a hint to verify, not a fact.
|
|
91
|
+
|
|
92
|
+
**Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue (a function name, error string, or config key), independent of whether that fix was ever linked to a filed issue:
|
|
93
|
+
```bash
|
|
94
|
+
git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
|
|
95
|
+
# Use -G instead of -S when the target is a regex pattern rather than a literal string
|
|
96
|
+
git log -G"{pattern}" --oneline -- {affected_files} | head -5
|
|
97
|
+
```
|
|
98
|
+
Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
|
|
82
99
|
6. **Determine root cause** — what's actually broken or missing?
|
|
83
100
|
7. **Identify affected files** — full list of files that need changes
|
|
84
101
|
8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
@@ -113,6 +130,12 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
113
130
|
### Evidence
|
|
114
131
|
{specific findings — function names, line numbers, behavior observed}
|
|
115
132
|
|
|
133
|
+
### History Findings
|
|
134
|
+
**Introducing commit**: {hash — author — date — subject, per primary affected file}
|
|
135
|
+
**Last touched**: {hash — author — date — subject}
|
|
136
|
+
**Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
|
|
137
|
+
{This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
|
|
138
|
+
|
|
116
139
|
### Recommendation
|
|
117
140
|
{what to build/fix, concrete and actionable}
|
|
118
141
|
|
package/commands/work-on.md
CHANGED
|
@@ -96,6 +96,74 @@ Satellite repos (those without a `staging` branch) receive fast-lane PRs directl
|
|
|
96
96
|
|
|
97
97
|
## Phase 0: Resolve Issue & Load Context
|
|
98
98
|
|
|
99
|
+
### 0.0: Pre-Flight Checks (MANDATORY — run before any other Phase 0 step)
|
|
100
|
+
|
|
101
|
+
Validate the environment before the pipeline spends tokens. Each check fails fast with an actionable error and a pointer to the troubleshooting guide (`docs/site/troubleshooting.md`). Run all checks; report every failure, then STOP if any HARD check fails. <!-- Added: forge#1149 -->
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
PREFLIGHT_FAILED=0
|
|
105
|
+
|
|
106
|
+
# Check 1 — forge.yaml present (HARD)
|
|
107
|
+
if [ ! -f forge.yaml ]; then
|
|
108
|
+
echo "ERROR: forge.yaml not found in the repository root."
|
|
109
|
+
echo " Fix: run \`npx forgedock init\` to generate one, or copy forge.yaml.example."
|
|
110
|
+
echo " See: docs/site/troubleshooting.md#1-forgeyaml-not-found"
|
|
111
|
+
PREFLIGHT_FAILED=1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Check 2 — yq installed; forge.yaml is valid YAML (HARD, only if present)
|
|
115
|
+
if [ -f forge.yaml ]; then
|
|
116
|
+
if ! command -v yq >/dev/null 2>&1; then
|
|
117
|
+
echo "ERROR: yq is not installed. The pipeline requires yq to parse forge.yaml."
|
|
118
|
+
echo " Fix: install yq — https://github.com/mikefarah/yq#install"
|
|
119
|
+
echo " See: docs/site/troubleshooting.md#2-forgeyaml-has-a-syntax-error"
|
|
120
|
+
PREFLIGHT_FAILED=1
|
|
121
|
+
elif ! yq '.' forge.yaml >/dev/null 2>&1; then
|
|
122
|
+
echo "ERROR: forge.yaml has a YAML syntax error."
|
|
123
|
+
echo " Fix: run \`yq '.' forge.yaml\` to locate the offending line, then correct the indentation/quoting."
|
|
124
|
+
echo " See: docs/site/troubleshooting.md#2-forgeyaml-has-a-syntax-error"
|
|
125
|
+
PREFLIGHT_FAILED=1
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Check 3 — gh CLI authenticated (HARD)
|
|
130
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
131
|
+
echo "ERROR: gh CLI is not authenticated. The pipeline cannot read or write GitHub state."
|
|
132
|
+
echo " Fix: run \`gh auth login\` (ensure repo scope), then \`gh auth status\` to confirm."
|
|
133
|
+
echo " See: docs/site/troubleshooting.md#3-gh-cli-not-authenticated"
|
|
134
|
+
PREFLIGHT_FAILED=1
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Check 4 — workflow labels exist on the repo (SOFT — warn, auto-recoverable)
|
|
138
|
+
if [ -f forge.yaml ] && gh auth status >/dev/null 2>&1; then
|
|
139
|
+
GH_REPO_PF="$(yq -r '.project.owner + "/" + .project.repo' forge.yaml 2>/dev/null)"
|
|
140
|
+
if [ -n "$GH_REPO_PF" ] && ! gh label list -R "$GH_REPO_PF" --search "workflow:" 2>/dev/null | grep -q "workflow:"; then
|
|
141
|
+
echo "WARNING: ForgeDock workflow:* labels not found on $GH_REPO_PF."
|
|
142
|
+
echo " Fix: run \`npx forgedock labels setup\` (or \`--repo $GH_REPO_PF\`) to bootstrap them."
|
|
143
|
+
echo " See: docs/site/troubleshooting.md#9-missing-workflow-labels"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Check 5 — GitHub API rate limit headroom (SOFT — warn)
|
|
148
|
+
if gh auth status >/dev/null 2>&1; then
|
|
149
|
+
RL_REMAINING="$(gh api rate_limit --jq '.resources.core.remaining' 2>/dev/null || echo '')"
|
|
150
|
+
if [ -n "$RL_REMAINING" ] && [ "$RL_REMAINING" -lt 100 ] 2>/dev/null; then
|
|
151
|
+
RL_RESET="$(gh api rate_limit --jq '.resources.core.reset' 2>/dev/null)"
|
|
152
|
+
echo "WARNING: GitHub API rate limit low ($RL_REMAINING remaining; resets at epoch $RL_RESET)."
|
|
153
|
+
echo " Fix: wait for the reset, reduce orchestration parallelism, or use a higher-limit PAT."
|
|
154
|
+
echo " See: docs/site/troubleshooting.md#10-github-api-rate-limit-exceeded"
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ "$PREFLIGHT_FAILED" -eq 1 ]; then
|
|
159
|
+
echo "Pre-flight checks failed. Resolve the errors above and re-run /work-on {NUMBER}."
|
|
160
|
+
echo "Full recovery guide: docs/site/troubleshooting.md"
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Worktree/branch-already-exists and stale-label conditions are surfaced later (Phase 3E worktree creation and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
|
|
166
|
+
|
|
99
167
|
### 0A: Parse input
|
|
100
168
|
Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human` and `workflow:decomposed`, pick highest priority.
|
|
101
169
|
|
|
@@ -424,7 +492,16 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
|
|
|
424
492
|
grep -rn '\.field_name\s*=' services/ # Attribute assignments
|
|
425
493
|
```
|
|
426
494
|
If the field is written with different types in different code paths (e.g. dict in the standard path, string in the auth-gated path), document ALL variants. The fix must handle every variant — not just the one on the primary investigated code path. A type guard like `or {}` only protects against falsy values; a non-empty string is truthy and bypasses it.
|
|
427
|
-
4. Git blame — trace when/why the relevant code was written
|
|
495
|
+
4. Git blame — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
|
|
496
|
+
```bash
|
|
497
|
+
# Introducing commit for each affected file (first commit that added it)
|
|
498
|
+
git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
|
|
499
|
+
# Last-touch commit (most recent change)
|
|
500
|
+
git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
|
|
501
|
+
# Line-level blame for a specific suspect hunk, if the issue names one
|
|
502
|
+
git blame -L {start},{end} -- {affected_file}
|
|
503
|
+
```
|
|
504
|
+
Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
|
|
428
505
|
4.5. **Rogue commit pre-state comparison (conditional)**: If the issue body references a specific commit as rogue, bad, or unintended (e.g., "rogue commit `abc1234`", "bad commit", "this was never intended"), MUST run `git show {commit}^:{file}` to see the file before that commit. Compare the pre-commit state against the current file. Any block present in the current file but absent in the pre-commit state was introduced by that commit chain and is a candidate for full reversion — not just partial editing. Report the delta (pre vs. current) in the investigation report. Do NOT assume surrounding code near a named import/bug is correct simply because the issue only named a specific sub-problem. (Ref: forge#278 — investigator confirmed the broken import but never ran `git show 18a3a2cf3^:batch.py`; the surrounding 50-line feature gate was also rogue and was preserved by the fix PR, causing a P1 access regression for all non-Scale users)
|
|
429
506
|
5. Domain context discovery (narrow scope only, 1–5 files):
|
|
430
507
|
```bash
|
|
@@ -432,6 +509,14 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
|
|
|
432
509
|
gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
|
|
433
510
|
```
|
|
434
511
|
Keep only file/function-level overlap. Max 5 related issues.
|
|
512
|
+
|
|
513
|
+
**Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue, independent of whether that fix was ever linked to a filed issue:
|
|
514
|
+
```bash
|
|
515
|
+
git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
|
|
516
|
+
# Use -G instead of -S when the target is a regex pattern rather than a literal string
|
|
517
|
+
git log -G"{pattern}" --oneline -- {affected_files} | head -5
|
|
518
|
+
```
|
|
519
|
+
Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
|
|
435
520
|
6. Determine root cause
|
|
436
521
|
7. Identify affected files — full list of files that need changes
|
|
437
522
|
7.5. **Sibling Pattern Sweep** *(conditional — when the bug is a condition, gated function call, or field presence check)*: After identifying the affected files, grep for the same pattern in sibling files within the same directory. The issue spec may name only the file where the error was first observed — but the same commit or PR that introduced the bug often applied it uniformly across related handlers.
|
|
@@ -475,6 +560,12 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
475
560
|
### Evidence
|
|
476
561
|
{specific findings — function names, line numbers, behavior observed}
|
|
477
562
|
|
|
563
|
+
### History Findings
|
|
564
|
+
**Introducing commit**: {hash — author — date — subject, per primary affected file}
|
|
565
|
+
**Last touched**: {hash — author — date — subject}
|
|
566
|
+
**Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
|
|
567
|
+
{This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
|
|
568
|
+
|
|
478
569
|
### Recommendation
|
|
479
570
|
{what to build/fix, concrete and actionable}
|
|
480
571
|
|
|
@@ -889,7 +980,12 @@ Run these queries (20s timeout each, 2 min total budget):
|
|
|
889
980
|
|
|
890
981
|
**C1: Past Review Findings on These Files**
|
|
891
982
|
```bash
|
|
892
|
-
|
|
983
|
+
# {AFFECTED_FILES} is a space-separated argument (see --files contract) — split
|
|
984
|
+
# explicitly on IFS=' ' into an array instead of a bare `for file in {AFFECTED_FILES}`,
|
|
985
|
+
# which word-splits on the shell's default IFS (space, tab, AND newline) and
|
|
986
|
+
# would corrupt any path containing a space.
|
|
987
|
+
IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
|
|
988
|
+
for file in "${AFFECTED_FILES_ARR[@]}"; do
|
|
893
989
|
basename=$(basename "$file" .py)
|
|
894
990
|
gh issue list -R {GH_REPO} --state closed --label "review-finding" \
|
|
895
991
|
--search "$basename" --limit 10 \
|
|
@@ -908,6 +1004,20 @@ git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head
|
|
|
908
1004
|
# For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
|
|
909
1005
|
```
|
|
910
1006
|
|
|
1007
|
+
**Direct commit-body read (bounded, prefer over `gh api` when it already answers "why")**: read the top 5 commit subjects+bodies on the affected files directly — local git is near-free relative to `gh api` round-trips, and commit bodies often explain the "why" without needing to fetch a linked issue at all:
|
|
1008
|
+
```bash
|
|
1009
|
+
git log -5 --format='%h %ad %s%n%b' --date=short -- {AFFECTED_FILES}
|
|
1010
|
+
```
|
|
1011
|
+
If a commit body fully explains a prior bug/fix (common for squashed or fix-up commits with no `#NNN` reference), use it directly as a "Past Bug in This Module" entry — do not require a linked GitHub issue to exist.
|
|
1012
|
+
|
|
1013
|
+
**Pickaxe pass (has this exact area been fixed before?)** — one bounded pass, capped at 5 hits, keyed on the suspected symbol/string from the Builder Contract or investigation report:
|
|
1014
|
+
```bash
|
|
1015
|
+
git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
|
|
1016
|
+
# Use -G instead of -S for regex patterns
|
|
1017
|
+
git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
|
|
1018
|
+
```
|
|
1019
|
+
Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
|
|
1020
|
+
|
|
911
1021
|
**C3: Related Code Paths** (callers/importers of FUNCTION_NAMES)
|
|
912
1022
|
```bash
|
|
913
1023
|
for fn in {FUNCTION_NAMES}; do
|
|
@@ -934,7 +1044,8 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
|
|
|
934
1044
|
{past review-finding issues}
|
|
935
1045
|
|
|
936
1046
|
### Past Bugs in This Module
|
|
937
|
-
{closed bug issues from git log mining
|
|
1047
|
+
{closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or commit
|
|
1048
|
+
bodies read directly per the C2 direct-commit-body step)}
|
|
938
1049
|
|
|
939
1050
|
### Related Code Paths (must stay consistent)
|
|
940
1051
|
{files that import/call changed functions}
|
|
@@ -1257,10 +1368,11 @@ Skip if no changed Python files contain DB engine/session/pool patterns.
|
|
|
1257
1368
|
|
|
1258
1369
|
```bash
|
|
1259
1370
|
cd {WORKTREE_PATH}
|
|
1260
|
-
|
|
1371
|
+
while IFS= read -r f; do
|
|
1372
|
+
[ -z "$f" ] && continue
|
|
1261
1373
|
grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
|
|
1262
1374
|
echo "DB CONFIG CHANGE DETECTED in: $f"
|
|
1263
|
-
done
|
|
1375
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
1264
1376
|
```
|
|
1265
1377
|
|
|
1266
1378
|
Advisory only — does not block build. Check for lambda/callable in connect_args (the exact bug class from PR #14391).
|
|
@@ -1335,7 +1447,7 @@ Stage all changes and commit:
|
|
|
1335
1447
|
```bash
|
|
1336
1448
|
cd {WORKTREE_PATH}
|
|
1337
1449
|
git add -u
|
|
1338
|
-
git commit -m "fix({SCOPE}): {description} (#{NUMBER})"
|
|
1450
|
+
git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
|
|
1339
1451
|
```
|
|
1340
1452
|
|
|
1341
1453
|
Conventional prefix: `fix`/`feat`/`refactor`/`docs`. Reference `#{NUMBER}` in message.
|
|
@@ -1649,7 +1761,14 @@ fi
|
|
|
1649
1761
|
|
|
1650
1762
|
## Phase 7: Summary & Trajectory
|
|
1651
1763
|
|
|
1652
|
-
### 7A: Report
|
|
1764
|
+
### 7A: Report + Pipeline Summary Card
|
|
1765
|
+
|
|
1766
|
+
Output the terse report, then render the shareable **Pipeline Summary Card** — the shareable
|
|
1767
|
+
moment a developer screenshots. Gather real stats (commits, additions/deletions, PR target,
|
|
1768
|
+
review summary, elapsed time) and render exactly as specified in `work-on/close.md` Phase C4.5
|
|
1769
|
+
(`C4.5a` stats gathering → `C4.5b` box-drawing card to stdout → `C4.5c` machine-readable twin).
|
|
1770
|
+
This inline path and the delegated `close.md` path MUST produce an identical card.
|
|
1771
|
+
|
|
1653
1772
|
```
|
|
1654
1773
|
## Done: #{NUMBER} — {TITLE}
|
|
1655
1774
|
- Investigation: {VERDICT} ({CONFIDENCE})
|
|
@@ -1658,6 +1777,98 @@ fi
|
|
|
1658
1777
|
- Files changed: {COUNT}
|
|
1659
1778
|
```
|
|
1660
1779
|
|
|
1780
|
+
Then print the card to stdout (inner width 51; truncate long titles with `…`; missing stats
|
|
1781
|
+
render `—`; pipeline line reflects the actual terminal state — merged / decomposed / invalid /
|
|
1782
|
+
blocked; draft PRs append `(draft)`):
|
|
1783
|
+
|
|
1784
|
+
```
|
|
1785
|
+
╔═══════════════════════════════════════════════════╗
|
|
1786
|
+
║ ForgeDock Pipeline Complete ║
|
|
1787
|
+
╠═══════════════════════════════════════════════════╣
|
|
1788
|
+
║ ║
|
|
1789
|
+
║ Issue: #{NUMBER} — {TITLE} ║
|
|
1790
|
+
║ Pipeline: investigate → architect → build → ║
|
|
1791
|
+
║ review → merge ✓ ║
|
|
1792
|
+
║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
|
|
1793
|
+
║ PR: #{PR_NUMBER} (merged to {PR_BASE}) ║
|
|
1794
|
+
║ Review: {REVIEW_SUMMARY} ║
|
|
1795
|
+
║ Time: {ELAPSED} ║
|
|
1796
|
+
║ ║
|
|
1797
|
+
╚═══════════════════════════════════════════════════╝
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
**Gather real stats** (C4.5a — this block MUST run on the inline path to populate card variables;
|
|
1801
|
+
do NOT rely on the cross-reference to `close.md` alone):
|
|
1802
|
+
|
|
1803
|
+
```bash
|
|
1804
|
+
PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
|
|
1805
|
+
COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
|
|
1806
|
+
ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
|
|
1807
|
+
DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
|
|
1808
|
+
PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
|
|
1809
|
+
IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
|
|
1810
|
+
|
|
1811
|
+
REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
1812
|
+
--jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
|
|
1813
|
+
# NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
|
|
1814
|
+
# `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
|
|
1815
|
+
# and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
|
|
1816
|
+
APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
|
|
1817
|
+
CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
|
|
1818
|
+
TOTAL_AGENTS=$((APPROVED + CHANGES))
|
|
1819
|
+
BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
|
|
1820
|
+
if [ "$TOTAL_AGENTS" -gt 0 ]; then
|
|
1821
|
+
REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
|
|
1822
|
+
else
|
|
1823
|
+
REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
|
|
1824
|
+
fi
|
|
1825
|
+
|
|
1826
|
+
FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1827
|
+
--jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
|
|
1828
|
+
if [ -n "$FIRST_TS" ]; then
|
|
1829
|
+
START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
|
|
1830
|
+
|| python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
|
|
1831
|
+
|| echo "")
|
|
1832
|
+
NOW_EPOCH=$(date -u +%s)
|
|
1833
|
+
if [ -n "$START_EPOCH" ]; then
|
|
1834
|
+
ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
|
|
1835
|
+
ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
|
|
1836
|
+
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
1837
|
+
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
1838
|
+
|
|
1839
|
+
case "{TERMINAL_STATE}" in
|
|
1840
|
+
decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
|
|
1841
|
+
invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
|
|
1842
|
+
blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
|
|
1843
|
+
*) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
|
|
1844
|
+
esac
|
|
1845
|
+
[ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
**Build the machine-readable twin** (C4.5c — MUST run this block to assign `CARD_JSON` before
|
|
1849
|
+
Phase 7B embeds it; the cross-reference to `close.md` above is insufficient on the inline path): <!-- forge#1178 -->
|
|
1850
|
+
|
|
1851
|
+
```bash
|
|
1852
|
+
CARD_JSON=$(jq -nc \
|
|
1853
|
+
--argjson issue {NUMBER} \
|
|
1854
|
+
--arg title "{TITLE}" \
|
|
1855
|
+
--arg status "$CARD_STATUS" \
|
|
1856
|
+
--arg pipeline "$PIPELINE_LINE" \
|
|
1857
|
+
--arg pr "{PR_NUMBER}" \
|
|
1858
|
+
--arg target "$PR_TARGET" \
|
|
1859
|
+
--arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
|
|
1860
|
+
--arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
|
|
1861
|
+
--argjson elapsed "${ELAPSED_SECS:-0}" \
|
|
1862
|
+
'{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
|
|
1863
|
+
pr:($pr|tonumber? // null), pr_target:$target,
|
|
1864
|
+
commits:($commits|tonumber? // null),
|
|
1865
|
+
additions:($adds|tonumber? // null),
|
|
1866
|
+
deletions:($dels|tonumber? // null),
|
|
1867
|
+
review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
`CARD_JSON` is now set and embedded in the trajectory comment by 7B.
|
|
1871
|
+
|
|
1661
1872
|
### 7B: Trajectory Log (MANDATORY)
|
|
1662
1873
|
|
|
1663
1874
|
**Review-presence check** (run before filling in Phase 4-5 row): <!-- Added: forge#381 -->
|
|
@@ -1691,9 +1902,16 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
|
|
|
1691
1902
|
|
|
1692
1903
|
**Decisions**: {key decisions}
|
|
1693
1904
|
**Anomalies**: {anomalies or None}
|
|
1694
|
-
**Pipeline completed**: {TIMESTAMP}
|
|
1905
|
+
**Pipeline completed**: {TIMESTAMP}
|
|
1906
|
+
|
|
1907
|
+
<!-- FORGE:CARD ${CARD_JSON} -->"
|
|
1695
1908
|
```
|
|
1696
1909
|
|
|
1910
|
+
Append the `<!-- FORGE:CARD {...} -->` block (machine-readable twin from 7A / close.md C4.5c)
|
|
1911
|
+
as the last line of the trajectory comment. It is HTML-comment-wrapped so it stays hidden in
|
|
1912
|
+
the rendered view but greppable for platform consumption (`/orchestrate` Phase 6 reads it for
|
|
1913
|
+
per-issue cards). Additive — does not affect existing `FORGE:TRAJECTORY` table consumers.
|
|
1914
|
+
|
|
1697
1915
|
### 7C: Graph Decision Record (MANDATORY when PR exists)
|
|
1698
1916
|
|
|
1699
1917
|
**Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
|