@aion0/forge 0.8.4 → 0.8.6
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/RELEASE_NOTES.md +4 -8
- package/app/api/agents/route.ts +11 -1
- package/app/api/jobs/preview/route.ts +54 -5
- package/app/api/jobs/recipes/route.ts +59 -0
- package/app/api/skills/install-local/route.ts +54 -1
- package/app/api/workflows/marketplace/route.ts +52 -0
- package/bin/forge-server.mjs +21 -0
- package/components/PipelineView.tsx +255 -7
- package/components/SettingsModal.tsx +45 -10
- package/components/SkillsPanel.tsx +178 -22
- package/components/WorkspaceView.tsx +3 -1
- package/install.sh +28 -0
- package/lib/agents/index.ts +6 -1
- package/lib/chat/agent-loop.ts +37 -3
- package/lib/chat/llm/anthropic.ts +22 -4
- package/lib/chat/protocols/http.ts +46 -2
- package/lib/chat/tool-dispatcher.ts +21 -3
- package/lib/jobs/recipes.ts +247 -0
- package/lib/jobs/scheduler.ts +17 -2
- package/lib/pipeline.ts +5 -610
- package/lib/settings.ts +6 -0
- package/lib/workflow-marketplace.ts +287 -0
- package/package.json +1 -1
package/lib/pipeline.ts
CHANGED
|
@@ -252,615 +252,6 @@ nodes:
|
|
|
252
252
|
outputs:
|
|
253
253
|
- name: result
|
|
254
254
|
extract: stdout
|
|
255
|
-
`,
|
|
256
|
-
'mantis-bug-fix-and-mr': `
|
|
257
|
-
name: mantis-bug-fix-and-mr
|
|
258
|
-
description: "Fetch Mantis bug context → worktree → fix code via headless Claude → push branch → open GitLab MR via glab CLI → notify assignee + reporter on Teams."
|
|
259
|
-
input:
|
|
260
|
-
bug_id: "Mantis bug id (number)"
|
|
261
|
-
project: "Forge project name"
|
|
262
|
-
base_branch: "Target branch the MR will be opened against (e.g. release/25.4). Required — set in the Job's input_template from the bug's product_version or target_branch field."
|
|
263
|
-
summary: "Bug summary (one line)"
|
|
264
|
-
description: "Full bug description (free text)"
|
|
265
|
-
priority: "Bug priority (optional)"
|
|
266
|
-
category: "Bug category (optional)"
|
|
267
|
-
reporter: "Mantis reporter username — used to notify them on Teams"
|
|
268
|
-
assignee: "Mantis assignee username — used to notify them on Teams"
|
|
269
|
-
extra_context: "Extra hints for Claude (optional)"
|
|
270
|
-
mr_title_template: "MR title template. Vars: {bug_id} {summary}. Default: 'Fix Mantis #{bug_id}: {summary}'"
|
|
271
|
-
mr_body_template: "MR body. Vars: {bug_id} {summary} {description} {claude_summary}. Default closes-reference + Claude summary."
|
|
272
|
-
teams_message_template: "Teams DM template. Vars: {bug_id} {summary} {mr_url} {role}. Default: '🤖 Mantis #{bug_id} fixed — review MR: {mr_url}'"
|
|
273
|
-
nodes:
|
|
274
|
-
resolve:
|
|
275
|
-
mode: shell
|
|
276
|
-
project: "{{input.project}}"
|
|
277
|
-
worktree: false
|
|
278
|
-
prompt: |
|
|
279
|
-
set -e
|
|
280
|
-
cd "$(git rev-parse --show-toplevel)"
|
|
281
|
-
command -v glab >/dev/null || { echo "ERROR: glab CLI not installed. brew install glab && glab auth login" >&2; exit 1; }
|
|
282
|
-
BUG_ID="{{input.bug_id}}"
|
|
283
|
-
BASE="{{input.base_branch}}"
|
|
284
|
-
[ -z "$BUG_ID" ] && { echo "ERROR: bug_id is required" >&2; exit 1; }
|
|
285
|
-
[ -z "$BASE" ] && { echo "ERROR: base_branch is required (set via Job input_template)" >&2; exit 1; }
|
|
286
|
-
REMOTE=$(git remote get-url origin)
|
|
287
|
-
RAW=$(echo "$REMOTE" | sed -E 's#^(https?://[^/]+/|git@[^:]+:)##; s#\\.git$##')
|
|
288
|
-
HOST=$(echo "$REMOTE" | sed -E 's#^https?://##; s#git@##; s#[:/].*##')
|
|
289
|
-
PROJECT_PATH="$RAW"
|
|
290
|
-
echo "HOST=$HOST"
|
|
291
|
-
echo "PROJECT_PATH=$PROJECT_PATH"
|
|
292
|
-
echo "BUG_ID=$BUG_ID"
|
|
293
|
-
echo "BASE=$BASE"
|
|
294
|
-
git fetch origin "$BASE" --quiet 2>/dev/null || true
|
|
295
|
-
outputs:
|
|
296
|
-
- name: info
|
|
297
|
-
extract: stdout
|
|
298
|
-
worktree-setup:
|
|
299
|
-
mode: shell
|
|
300
|
-
project: "{{input.project}}"
|
|
301
|
-
worktree: false
|
|
302
|
-
depends_on: [resolve]
|
|
303
|
-
prompt: |
|
|
304
|
-
set -e
|
|
305
|
-
INFO=$'{{nodes.resolve.outputs.info}}'
|
|
306
|
-
eval "$(echo "$INFO" | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"
|
|
307
|
-
# worktree: false above keeps us in the project root, so this is fine.
|
|
308
|
-
# Falls back to FORGE_PROJECT_ROOT just in case auto-worktree gets
|
|
309
|
-
# re-enabled in the future.
|
|
310
|
-
ROOT="\${FORGE_PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
|
|
311
|
-
cd "$ROOT"
|
|
312
|
-
WORKTREE_DIR="$ROOT/.forge/worktrees/mantis-$BUG_ID"
|
|
313
|
-
BRANCH="fix/mantis-\${BUG_ID}"
|
|
314
|
-
|
|
315
|
-
# ALL the noisy git operations route to stderr — downstream nodes
|
|
316
|
-
# consume \`wt\` via \`eval "$(echo … | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"\` inside
|
|
317
|
-
# \`$'…'\` quoting. ANY apostrophe / paren / space-ID-paren in the
|
|
318
|
-
# captured stdout (e.g. "Deleted branch fix/X (was abc1234).",
|
|
319
|
-
# "Preparing worktree (resetting branch 'X')") breaks bash with
|
|
320
|
-
# "syntax error near unexpected token '('".
|
|
321
|
-
{
|
|
322
|
-
# 1. Remove the on-disk worktree for THIS bug (idempotent).
|
|
323
|
-
if [ -d "$WORKTREE_DIR" ]; then
|
|
324
|
-
git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR"
|
|
325
|
-
fi
|
|
326
|
-
# 2. Drop stale worktree registrations + any other worktree pinning
|
|
327
|
-
# our branch. Without (3), "fatal: <branch> already used by
|
|
328
|
-
# worktree at <path>" can fire on the next add.
|
|
329
|
-
git worktree prune
|
|
330
|
-
git worktree list --porcelain | awk -v b="refs/heads/$BRANCH" '
|
|
331
|
-
/^worktree /{w=$2}
|
|
332
|
-
$0=="branch "b{print w}
|
|
333
|
-
' | while read w; do
|
|
334
|
-
[ -n "$w" ] && git worktree remove --force "$w" 2>/dev/null || true
|
|
335
|
-
done
|
|
336
|
-
git branch -D "$BRANCH" 2>/dev/null || true
|
|
337
|
-
mkdir -p "$ROOT/.forge/worktrees"
|
|
338
|
-
# -B (capital) = create-or-reset; --force lets us reuse an existing dir.
|
|
339
|
-
git worktree add --force -B "$BRANCH" "$WORKTREE_DIR" "origin/$BASE"
|
|
340
|
-
} 1>&2
|
|
341
|
-
|
|
342
|
-
# IMPORTANT: emit ONLY the worktree path on stdout — nothing else.
|
|
343
|
-
# fix-code uses \`workdir: {{nodes.worktree-setup.outputs.wt}}\` and the
|
|
344
|
-
# task runner needs a clean path. If we echo'd "WORKTREE=…\\nBRANCH=…"
|
|
345
|
-
# the multi-line string became an invalid workdir → Claude silently
|
|
346
|
-
# ran in the pipeline's parent worktree, made its changes there, and
|
|
347
|
-
# nothing showed up in the inner branch when push-and-mr cd'd in.
|
|
348
|
-
# Downstream nodes get the branch by asking git from inside the
|
|
349
|
-
# worktree (git symbolic-ref --short HEAD) — no out-of-band passing.
|
|
350
|
-
echo "$WORKTREE_DIR"
|
|
351
|
-
outputs:
|
|
352
|
-
- name: wt
|
|
353
|
-
extract: stdout
|
|
354
|
-
fetch-bug-details:
|
|
355
|
-
mode: shell
|
|
356
|
-
project: "{{input.project}}"
|
|
357
|
-
worktree: false
|
|
358
|
-
depends_on: [resolve]
|
|
359
|
-
prompt: |
|
|
360
|
-
# Pull the FULL mantis bug — summary, description, additional_information,
|
|
361
|
-
# reproducibility, all notes with author/date, history — via Forge's
|
|
362
|
-
# loopback connector-tool endpoint. search_bugs (when used as the Job
|
|
363
|
-
# source) only returns list-page columns; this node fills the gap so
|
|
364
|
-
# fix-code has every field it could possibly want.
|
|
365
|
-
set -e
|
|
366
|
-
INFO=$'{{nodes.resolve.outputs.info}}'
|
|
367
|
-
eval "$(echo "$INFO" | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"
|
|
368
|
-
PAYLOAD="{\\"plugin_id\\":\\"mantis\\",\\"tool\\":\\"get_bug\\",\\"input\\":{\\"id\\":$BUG_ID}}"
|
|
369
|
-
RESP=$(curl -sS -X POST http://127.0.0.1:8403/api/connector-tool \\
|
|
370
|
-
-H 'content-type: application/json' --data "$PAYLOAD")
|
|
371
|
-
IS_ERR=$(echo "$RESP" | jq -r '.is_error // false')
|
|
372
|
-
if [ "$IS_ERR" = "true" ]; then
|
|
373
|
-
echo "WARN: mantis.get_bug failed: $(echo "$RESP" | jq -r '.content // .error // "(unknown)"' | head -c 300)" >&2
|
|
374
|
-
echo "BUG_ID=$BUG_ID"
|
|
375
|
-
echo "BUG_JSON_B64="
|
|
376
|
-
echo "SUMMARY="
|
|
377
|
-
echo "CATEGORY="
|
|
378
|
-
echo "DESCRIPTION_B64="
|
|
379
|
-
echo "ADDITIONAL_INFO_B64="
|
|
380
|
-
echo "NOTES_B64="
|
|
381
|
-
exit 0
|
|
382
|
-
fi
|
|
383
|
-
# .content is itself a JSON string (the bug object); decode once.
|
|
384
|
-
BUG=$(echo "$RESP" | jq -r '.content' | jq -c '.')
|
|
385
|
-
|
|
386
|
-
# Whole bug as base64 — gives fix-code an escape hatch to read any
|
|
387
|
-
# field we didn't bother to enumerate (history, _fields_raw, etc).
|
|
388
|
-
BUG_JSON_B64=$(echo -n "$BUG" | base64 | tr -d '\\n')
|
|
389
|
-
|
|
390
|
-
# Convenience extracts. Multi-line fields (description, notes,
|
|
391
|
-
# additional_information) → base64'd so newlines / quotes / shell
|
|
392
|
-
# metachars survive transport. Single-line stays plain for the
|
|
393
|
-
# KEY=value eval pattern downstream nodes already use.
|
|
394
|
-
SUMMARY=$(echo "$BUG" | jq -r '.summary // ""' | head -c 500 | tr -d '\\n')
|
|
395
|
-
CAT=$(echo "$BUG" | jq -r '.category // ""' | tr -d '\\n')
|
|
396
|
-
REPRO=$(echo "$BUG" | jq -r '.reproducibility // ""' | tr -d '\\n')
|
|
397
|
-
DESC_B64=$(echo "$BUG" | jq -r '.description // ""' | base64 | tr -d '\\n')
|
|
398
|
-
ADDL_B64=$(echo "$BUG" | jq -r '.additional_information // ""' | base64 | tr -d '\\n')
|
|
399
|
-
# Notes with author + date + body, separated by ─── for readability.
|
|
400
|
-
NOTES_B64=$(echo "$BUG" | jq -r '[.notes[]? | "[\\(.author // "?") @ \\(.date // "?")]\\n\\(.body // "")"] | join("\\n\\n─────────────\\n\\n")' 2>/dev/null | base64 | tr -d '\\n')
|
|
401
|
-
|
|
402
|
-
echo "BUG_ID=$BUG_ID"
|
|
403
|
-
echo "SUMMARY=$SUMMARY"
|
|
404
|
-
echo "CATEGORY=$CAT"
|
|
405
|
-
echo "REPRODUCIBILITY=$REPRO"
|
|
406
|
-
echo "DESCRIPTION_B64=$DESC_B64"
|
|
407
|
-
echo "ADDITIONAL_INFO_B64=$ADDL_B64"
|
|
408
|
-
echo "NOTES_B64=$NOTES_B64"
|
|
409
|
-
echo "BUG_JSON_B64=$BUG_JSON_B64"
|
|
410
|
-
|
|
411
|
-
# ── Human-readable preview to stderr ──
|
|
412
|
-
# Goes into the task log so the user can see what we actually got
|
|
413
|
-
# from Mantis without having to manually base64 -d. Truncated at
|
|
414
|
-
# 800 chars per field to keep the log tractable. Stdout above keeps
|
|
415
|
-
# the clean KEY=value pairs downstream nodes parse.
|
|
416
|
-
DESC_PLAIN=$(echo "$BUG" | jq -r '.description // ""')
|
|
417
|
-
ADDL_PLAIN=$(echo "$BUG" | jq -r '.additional_information // ""')
|
|
418
|
-
NOTES_COUNT=$(echo "$BUG" | jq -r '.notes | length // 0')
|
|
419
|
-
NOTES_FIRST=$(echo "$BUG" | jq -r '.notes[0]? | "[\\(.author // "?") @ \\(.date // "?")] \\(.body // "")"' 2>/dev/null | head -c 400)
|
|
420
|
-
NOTES_LAST=$(echo "$BUG" | jq -r '.notes[-1]? | "[\\(.author // "?") @ \\(.date // "?")] \\(.body // "")"' 2>/dev/null | head -c 400)
|
|
421
|
-
DESC_LEN=$(printf %s "$DESC_PLAIN" | wc -c | tr -d ' ')
|
|
422
|
-
ADDL_LEN=$(printf %s "$ADDL_PLAIN" | wc -c | tr -d ' ')
|
|
423
|
-
{
|
|
424
|
-
echo ""
|
|
425
|
-
echo "─── fetch-bug-details — what we got from Mantis #$BUG_ID ───"
|
|
426
|
-
echo "Summary: $SUMMARY"
|
|
427
|
-
echo "Category: $CAT"
|
|
428
|
-
echo "Reproducibility:$REPRO"
|
|
429
|
-
echo ""
|
|
430
|
-
echo "Description (\${DESC_LEN} chars, showing first 800):"
|
|
431
|
-
printf %s "$DESC_PLAIN" | head -c 800
|
|
432
|
-
[ "$DESC_LEN" -gt 800 ] && echo "… [truncated]"
|
|
433
|
-
echo ""
|
|
434
|
-
echo ""
|
|
435
|
-
echo "Additional Information (\${ADDL_LEN} chars, showing first 800):"
|
|
436
|
-
printf %s "$ADDL_PLAIN" | head -c 800
|
|
437
|
-
[ "$ADDL_LEN" -gt 800 ] && echo "… [truncated]"
|
|
438
|
-
echo ""
|
|
439
|
-
echo ""
|
|
440
|
-
echo "Notes: $NOTES_COUNT total"
|
|
441
|
-
if [ "$NOTES_COUNT" -gt 0 ]; then
|
|
442
|
-
echo " First: $NOTES_FIRST"
|
|
443
|
-
[ "$NOTES_COUNT" -gt 1 ] && echo " Last: $NOTES_LAST"
|
|
444
|
-
fi
|
|
445
|
-
echo "──────────────────────────────────────────────────────"
|
|
446
|
-
} >&2
|
|
447
|
-
outputs:
|
|
448
|
-
- name: details
|
|
449
|
-
extract: stdout
|
|
450
|
-
download-attachments:
|
|
451
|
-
mode: shell
|
|
452
|
-
project: "{{input.project}}"
|
|
453
|
-
worktree: false
|
|
454
|
-
depends_on: [resolve, worktree-setup]
|
|
455
|
-
prompt: |
|
|
456
|
-
# Download all Attached Files from the Mantis bug into the worktree's
|
|
457
|
-
# .attachments/ directory so fix-code (Claude with vision) can read
|
|
458
|
-
# screenshots, logs, and any other supporting material. Skips files
|
|
459
|
-
# larger than 5 MB (see download_attachment's max_size_bytes default).
|
|
460
|
-
set -e
|
|
461
|
-
INFO=$'{{nodes.resolve.outputs.info}}'
|
|
462
|
-
eval "$(echo "$INFO" | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"
|
|
463
|
-
WORKTREE_DIR=$'{{nodes.worktree-setup.outputs.wt}}'
|
|
464
|
-
WORKTREE_DIR=$(echo "$WORKTREE_DIR" | head -1 | tr -d '[:space:]')
|
|
465
|
-
ATTACH_DIR="$WORKTREE_DIR/.attachments"
|
|
466
|
-
mkdir -p "$ATTACH_DIR"
|
|
467
|
-
|
|
468
|
-
# 1. List attachments.
|
|
469
|
-
LIST=$(curl -sS -X POST http://127.0.0.1:8403/api/connector-tool \\
|
|
470
|
-
-H 'content-type: application/json' \\
|
|
471
|
-
--data "{\\"plugin_id\\":\\"mantis\\",\\"tool\\":\\"list_attachments\\",\\"input\\":{\\"id\\":$BUG_ID,\\"bug_id\\":$BUG_ID}}" )
|
|
472
|
-
IS_ERR=$(echo "$LIST" | jq -r '.is_error // false')
|
|
473
|
-
if [ "$IS_ERR" = "true" ]; then
|
|
474
|
-
echo "ATTACH_COUNT=0"
|
|
475
|
-
echo "ATTACH_DIR=$ATTACH_DIR"
|
|
476
|
-
echo "WARN: list_attachments failed: $(echo "$LIST" | jq -r '.content' | head -c 200)" >&2
|
|
477
|
-
exit 0
|
|
478
|
-
fi
|
|
479
|
-
ATTACHMENTS=$(echo "$LIST" | jq -r '.content' | jq -c '.attachments // []')
|
|
480
|
-
TOTAL=$(echo "$ATTACHMENTS" | jq 'length')
|
|
481
|
-
echo "Found $TOTAL attachment(s) on bug $BUG_ID"
|
|
482
|
-
|
|
483
|
-
COUNT=0
|
|
484
|
-
SKIPPED=0
|
|
485
|
-
# 2. Iterate + download each. jq stream the ids/filenames.
|
|
486
|
-
echo "$ATTACHMENTS" | jq -c '.[]' | while read -r ATT; do
|
|
487
|
-
FID=$(echo "$ATT" | jq -r '.file_id')
|
|
488
|
-
FN=$(echo "$ATT" | jq -r '.filename')
|
|
489
|
-
SZ=$(echo "$ATT" | jq -r '.size')
|
|
490
|
-
# Sanitize filename to avoid path traversal.
|
|
491
|
-
SAFE=$(echo "$FN" | tr -d '\\\\/:*?"<>|' | head -c 200)
|
|
492
|
-
[ -z "$SAFE" ] && SAFE="file-$FID"
|
|
493
|
-
echo " → $SAFE ($SZ bytes)"
|
|
494
|
-
RESP=$(curl -sS -X POST http://127.0.0.1:8403/api/connector-tool \\
|
|
495
|
-
-H 'content-type: application/json' \\
|
|
496
|
-
--data "{\\"plugin_id\\":\\"mantis\\",\\"tool\\":\\"download_attachment\\",\\"input\\":{\\"file_id\\":$FID}}")
|
|
497
|
-
if [ "$(echo "$RESP" | jq -r '.is_error // false')" = "true" ]; then
|
|
498
|
-
echo " WARN: download failed for $SAFE" >&2
|
|
499
|
-
continue
|
|
500
|
-
fi
|
|
501
|
-
BODY=$(echo "$RESP" | jq -r '.content')
|
|
502
|
-
SKIP=$(echo "$BODY" | jq -r '.skipped // false')
|
|
503
|
-
if [ "$SKIP" = "true" ]; then
|
|
504
|
-
REASON=$(echo "$BODY" | jq -r '.reason // ""')
|
|
505
|
-
echo " SKIPPED $SAFE — $REASON" >&2
|
|
506
|
-
SKIPPED=$((SKIPPED+1))
|
|
507
|
-
continue
|
|
508
|
-
fi
|
|
509
|
-
B64=$(echo "$BODY" | jq -r '.content_b64')
|
|
510
|
-
[ -z "$B64" ] && { echo " WARN: empty content_b64 for $SAFE" >&2; continue; }
|
|
511
|
-
echo "$B64" | base64 -d > "$ATTACH_DIR/$SAFE"
|
|
512
|
-
COUNT=$((COUNT+1))
|
|
513
|
-
done
|
|
514
|
-
|
|
515
|
-
echo "ATTACH_COUNT=$COUNT"
|
|
516
|
-
echo "ATTACH_SKIPPED=$SKIPPED"
|
|
517
|
-
echo "ATTACH_DIR=$ATTACH_DIR"
|
|
518
|
-
|
|
519
|
-
# Human-readable directory listing into stderr so the log shows
|
|
520
|
-
# what's actually on disk for the next node.
|
|
521
|
-
{
|
|
522
|
-
echo ""
|
|
523
|
-
echo "─── download-attachments — files in $ATTACH_DIR ───"
|
|
524
|
-
if [ -d "$ATTACH_DIR" ] && [ -n "$(ls -A "$ATTACH_DIR" 2>/dev/null)" ]; then
|
|
525
|
-
ls -lhS "$ATTACH_DIR" 2>/dev/null | tail -n +2
|
|
526
|
-
else
|
|
527
|
-
echo "(no files)"
|
|
528
|
-
fi
|
|
529
|
-
echo "──────────────────────────────────────────────────────"
|
|
530
|
-
} >&2
|
|
531
|
-
outputs:
|
|
532
|
-
- name: attach
|
|
533
|
-
extract: stdout
|
|
534
|
-
fix-code:
|
|
535
|
-
project: "{{input.project}}"
|
|
536
|
-
worktree: false
|
|
537
|
-
depends_on: [worktree-setup, fetch-bug-details, download-attachments]
|
|
538
|
-
workdir: "{{nodes.worktree-setup.outputs.wt}}"
|
|
539
|
-
prompt: |
|
|
540
|
-
A Mantis bug needs to be fixed in this worktree (already checked out
|
|
541
|
-
from the target base branch). You are running headless — make the fix
|
|
542
|
-
yourself, stage + commit.
|
|
543
|
-
|
|
544
|
-
## Bug
|
|
545
|
-
ID: {{input.bug_id}}
|
|
546
|
-
Priority: {{input.priority}}
|
|
547
|
-
Category (from get_bug, full): see DETAILS below
|
|
548
|
-
Assignee: {{input.assignee}}
|
|
549
|
-
Reporter: {{input.reporter}}
|
|
550
|
-
|
|
551
|
-
## Summary
|
|
552
|
-
{{input.summary}}
|
|
553
|
-
|
|
554
|
-
## Full bug details (fetched via mantis.get_bug)
|
|
555
|
-
The fetch-bug-details node ran \`mantis.get_bug\` and exported these
|
|
556
|
-
key=value lines. _B64 fields are base64-encoded multi-line strings:
|
|
557
|
-
|
|
558
|
-
{{nodes.fetch-bug-details.outputs.details}}
|
|
559
|
-
|
|
560
|
-
Decode any _B64 field with:
|
|
561
|
-
\`echo "<value>" | base64 -d\`
|
|
562
|
-
|
|
563
|
-
Available fields (READ ALL of them before deciding on a fix):
|
|
564
|
-
SUMMARY one-line title (plain)
|
|
565
|
-
CATEGORY Mantis category (plain)
|
|
566
|
-
REPRODUCIBILITY always / sometimes / random / N/A (plain)
|
|
567
|
-
DESCRIPTION_B64 full bug description (base64; often the most
|
|
568
|
-
important field — repro steps live here)
|
|
569
|
-
ADDITIONAL_INFO_B64 "Additional Information" custom field —
|
|
570
|
-
often holds stack traces, env details, log
|
|
571
|
-
snippets. Decode + read.
|
|
572
|
-
NOTES_B64 all comments concatenated, with author + date
|
|
573
|
-
headers ([author @ date]). Use to see what
|
|
574
|
-
QA + reporter already discussed.
|
|
575
|
-
BUG_JSON_B64 escape hatch — the whole bug object as JSON
|
|
576
|
-
(decoded, this has every field including
|
|
577
|
-
history[] and _fields_raw if you need it).
|
|
578
|
-
|
|
579
|
-
## Description (search_bugs-supplied, may be empty)
|
|
580
|
-
{{input.description}}
|
|
581
|
-
|
|
582
|
-
## Attached Files
|
|
583
|
-
{{nodes.download-attachments.outputs.attach}}
|
|
584
|
-
|
|
585
|
-
Files (if any) were saved under \`.attachments/\` in this worktree.
|
|
586
|
-
List them with \`ls -la .attachments/\`. For images, USE THE READ
|
|
587
|
-
TOOL on the file — your vision can analyze screenshots / error
|
|
588
|
-
dialogs / network diagrams directly. For text-like attachments
|
|
589
|
-
(log snippets, .txt, .json, small .pcap text exports) just open
|
|
590
|
-
them. Files larger than 5 MB were skipped — if a critical one is
|
|
591
|
-
missing, mention it in your fix notes; don't try to re-fetch.
|
|
592
|
-
|
|
593
|
-
## Extra context
|
|
594
|
-
{{input.extra_context}}
|
|
595
|
-
|
|
596
|
-
## HARD RULES — read before tool use
|
|
597
|
-
- **Never call mcp__* tools.** Pipeline tasks do not have interactive
|
|
598
|
-
auth; any MCP call that needs SSO/TOTP (mantis_auth, gitlab, pmdb,
|
|
599
|
-
…) will hang and fail the pipeline. The bug context above is the
|
|
600
|
-
only Mantis data you get.
|
|
601
|
-
- If a field above is empty or truncated, **work with what is given**.
|
|
602
|
-
Do NOT try to fetch more from Mantis. The pipeline owns Mantis access
|
|
603
|
-
upstream — re-running mantis lookup here is wasted effort and breaks
|
|
604
|
-
the design contract (Job → Pipeline data flow).
|
|
605
|
-
- If you ABSOLUTELY need supplemental data from another Forge connector
|
|
606
|
-
(rare), use the Forge HTTP API instead of MCP. POST
|
|
607
|
-
http://127.0.0.1:8403/api/connector-tool with JSON body
|
|
608
|
-
{"plugin_id":"<plugin>","tool":"<tool>","input":{...}}
|
|
609
|
-
|
|
610
|
-
## Steps
|
|
611
|
-
1. Read the bug carefully — identify the affected component.
|
|
612
|
-
2. Find the relevant file(s) via git grep + reading the code.
|
|
613
|
-
3. Implement a minimal fix.
|
|
614
|
-
4. Add tests if the codebase has a test suite.
|
|
615
|
-
5. Stage + commit with message: 'Fix Mantis #{{input.bug_id}}: <one-line>'.
|
|
616
|
-
**DO NOT add a "Co-Authored-By: Claude …" or "Generated with Claude
|
|
617
|
-
Code" trailer.** Plain commit message only — the MR is going into a
|
|
618
|
-
corporate repo and the AI co-authorship attribution is not wanted.
|
|
619
|
-
|
|
620
|
-
Do NOT push — the next pipeline node handles push + MR.
|
|
621
|
-
outputs:
|
|
622
|
-
- name: summary
|
|
623
|
-
extract: result
|
|
624
|
-
- name: diff
|
|
625
|
-
extract: git_diff
|
|
626
|
-
push-and-mr:
|
|
627
|
-
mode: shell
|
|
628
|
-
project: "{{input.project}}"
|
|
629
|
-
worktree: false
|
|
630
|
-
workdir: "{{nodes.worktree-setup.outputs.wt}}"
|
|
631
|
-
depends_on: [fix-code]
|
|
632
|
-
prompt: |
|
|
633
|
-
set -e
|
|
634
|
-
INFO=$'{{nodes.resolve.outputs.info}}'
|
|
635
|
-
eval "$(echo "$INFO" | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"
|
|
636
|
-
# workdir already cd'd us into the mantis worktree. Derive BRANCH.
|
|
637
|
-
BRANCH=$(git symbolic-ref --short HEAD)
|
|
638
|
-
# Bail if Claude didn't actually commit anything
|
|
639
|
-
AHEAD=$(git rev-list --count "origin/$BASE..HEAD" 2>/dev/null || echo 0)
|
|
640
|
-
if [ "$AHEAD" -lt 1 ]; then
|
|
641
|
-
echo "NO_CHANGES — Claude did not commit; aborting MR creation"
|
|
642
|
-
echo "MR_URL="
|
|
643
|
-
exit 0
|
|
644
|
-
fi
|
|
645
|
-
git push -u origin "$BRANCH" --force-with-lease 2>&1
|
|
646
|
-
# Heredoc keeps Claude's summary intact (apostrophes, parens, $, …).
|
|
647
|
-
SUMMARY=$(cat <<'FORGE_SUMMARY_EOF'
|
|
648
|
-
{{nodes.fix-code.outputs.summary}}
|
|
649
|
-
FORGE_SUMMARY_EOF
|
|
650
|
-
)
|
|
651
|
-
# ALL pipeline-templated values must enter via \`$'…'\` ANSI-C quoting,
|
|
652
|
-
# NOT plain "…". The pipeline engine ANSI-C-escapes substituted values
|
|
653
|
-
# (\\n / \\t / \\' etc.); inside "…" bash treats those as literal text
|
|
654
|
-
# and the MR description ends up showing "\\n" instead of real
|
|
655
|
-
# newlines, breaking the markdown.
|
|
656
|
-
TITLE_TPL=$'{{input.mr_title_template}}'
|
|
657
|
-
[ -z "$TITLE_TPL" ] && TITLE_TPL="Fix Mantis #{bug_id}: {summary}"
|
|
658
|
-
BODY_TPL=$'{{input.mr_body_template}}'
|
|
659
|
-
[ -z "$BODY_TPL" ] && BODY_TPL="Auto-fix for Mantis bug #{bug_id}.
|
|
660
|
-
|
|
661
|
-
## Summary
|
|
662
|
-
{summary}
|
|
663
|
-
|
|
664
|
-
## Original description
|
|
665
|
-
{description}
|
|
666
|
-
|
|
667
|
-
## Claude's fix notes
|
|
668
|
-
{claude_summary}
|
|
669
|
-
|
|
670
|
-
## Files changed
|
|
671
|
-
{diff_stat}
|
|
672
|
-
|
|
673
|
-
_Opened by Forge mantis-bug-fix-and-mr pipeline._"
|
|
674
|
-
BUG_ID_VAR=$'{{input.bug_id}}'
|
|
675
|
-
SUMMARY_VAR=$'{{input.summary}}'
|
|
676
|
-
DESCRIPTION_VAR=$'{{input.description}}'
|
|
677
|
-
# Compact list of files touched. --stat gives "N files changed, X+/Y-"
|
|
678
|
-
# which is small but tells the reviewer what's in the MR at a glance.
|
|
679
|
-
DIFF_STAT=$(git diff --stat "origin/$BASE..HEAD" 2>/dev/null || echo "")
|
|
680
|
-
MR_TITLE=$(python3 - "$TITLE_TPL" "$BUG_ID_VAR" "$SUMMARY_VAR" <<'PY'
|
|
681
|
-
import sys
|
|
682
|
-
tpl, bug_id, summary = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
683
|
-
print(tpl.replace('{bug_id}', bug_id).replace('{summary}', summary))
|
|
684
|
-
PY
|
|
685
|
-
)
|
|
686
|
-
MR_BODY=$(python3 - "$BODY_TPL" "$BUG_ID_VAR" "$SUMMARY_VAR" "$DESCRIPTION_VAR" "$SUMMARY" "$DIFF_STAT" <<'PY'
|
|
687
|
-
import sys
|
|
688
|
-
tpl, bug_id, summary, description, claude, diff_stat = sys.argv[1:7]
|
|
689
|
-
out = (tpl.replace('{bug_id}', bug_id)
|
|
690
|
-
.replace('{summary}', summary)
|
|
691
|
-
.replace('{description}', description)
|
|
692
|
-
.replace('{claude_summary}', claude)
|
|
693
|
-
.replace('{diff_stat}', diff_stat))
|
|
694
|
-
print(out)
|
|
695
|
-
PY
|
|
696
|
-
)
|
|
697
|
-
# Force glab to use the Forge-managed GitLab PAT. Forge injects
|
|
698
|
-
# GITLAB_TOKEN from the gitlab connector's config; without
|
|
699
|
-
# rewriting it here, glab can stick to its stale per-host token
|
|
700
|
-
# in ~/.config/glab-cli/config.yml and 401 with "Token was
|
|
701
|
-
# revoked" — even when the env is set. glab auth login --token
|
|
702
|
-
# is idempotent and writes the right host config in 100 ms.
|
|
703
|
-
if [ -n "$GITLAB_TOKEN" ]; then
|
|
704
|
-
REMOTE_HOST_FOR_AUTH=$(git -C "$PROJECT_PATH" config --get remote.origin.url 2>/dev/null \\
|
|
705
|
-
| sed -E 's#^(https?://)?(git@)?([^:/]+).*#\\3#')
|
|
706
|
-
if [ -n "$REMOTE_HOST_FOR_AUTH" ]; then
|
|
707
|
-
echo "Refreshing glab auth for $REMOTE_HOST_FOR_AUTH from Forge connector token"
|
|
708
|
-
echo "$GITLAB_TOKEN" | glab auth login --hostname "$REMOTE_HOST_FOR_AUTH" --token-stdin >/dev/null 2>&1 || \\
|
|
709
|
-
echo "(glab auth login refresh failed — falling through; will likely 401)" >&2
|
|
710
|
-
fi
|
|
711
|
-
fi
|
|
712
|
-
|
|
713
|
-
# Bug assignee → MR reviewer. Mantis assignee comes as "Jane Doe (jdoe)";
|
|
714
|
-
# extract the (username) and pass to glab. If parens absent or empty,
|
|
715
|
-
# skip the flag — glab errors on --reviewer "" .
|
|
716
|
-
ASSIGNEE_RAW=$'{{input.assignee}}'
|
|
717
|
-
REVIEWER_USERNAME=$(echo "$ASSIGNEE_RAW" | grep -oE '\\(([a-zA-Z0-9._-]+)\\)' | tail -1 | tr -d '()')
|
|
718
|
-
REVIEWER_FLAGS=()
|
|
719
|
-
if [ -n "$REVIEWER_USERNAME" ]; then
|
|
720
|
-
REVIEWER_FLAGS+=(--reviewer "$REVIEWER_USERNAME")
|
|
721
|
-
echo "Setting MR reviewer: $REVIEWER_USERNAME (from assignee '$ASSIGNEE_RAW')"
|
|
722
|
-
else
|
|
723
|
-
echo "No (username) found in assignee '$ASSIGNEE_RAW' — skipping --reviewer"
|
|
724
|
-
fi
|
|
725
|
-
# Capture glab output AND exit code so we can debug failures. Old
|
|
726
|
-
# pipeline that just grep'd for the URL ate everything if the
|
|
727
|
-
# regex didn't match — leaving MR_URL empty with no clue why
|
|
728
|
-
# (auth refresh needed, reviewer username wrong, MR already exists
|
|
729
|
-
# at a non-canonical URL, etc).
|
|
730
|
-
set +e
|
|
731
|
-
GLAB_OUT=$(glab mr create -R "$PROJECT_PATH" --target-branch "$BASE" --source-branch "$BRANCH" \\
|
|
732
|
-
--title "$MR_TITLE" --description "$MR_BODY" \\
|
|
733
|
-
"\${REVIEWER_FLAGS[@]}" \\
|
|
734
|
-
--yes 2>&1)
|
|
735
|
-
GLAB_RC=$?
|
|
736
|
-
set -e
|
|
737
|
-
# Surface glab's output to the task log (truncated) so a failed
|
|
738
|
-
# retry doesn't disappear into the void.
|
|
739
|
-
echo "--- glab mr create (rc=$GLAB_RC) ---"
|
|
740
|
-
echo "$GLAB_OUT" | head -50
|
|
741
|
-
echo "--- end glab output ---"
|
|
742
|
-
|
|
743
|
-
# Common, very actionable failure: corp GitLab revoked the
|
|
744
|
-
# token. Forge already pushes the connector PAT here via
|
|
745
|
-
# GITLAB_TOKEN + a glab auth login --token-stdin refresh
|
|
746
|
-
# earlier in this step, so the only way to land here is if the
|
|
747
|
-
# connector PAT itself is wrong/revoked. Point the user at
|
|
748
|
-
# Settings, not at glab auth login.
|
|
749
|
-
if echo "$GLAB_OUT" | grep -qE '401|invalid_token|Token was revoked|unauthorized'; then
|
|
750
|
-
REMOTE_HOST=$(git config --get remote.origin.url 2>/dev/null | sed -E 's#^(https?://)?(git@)?([^:/]+).*#\\3#')
|
|
751
|
-
echo "ERROR: GitLab rejected the token for \${REMOTE_HOST:-this GitLab server}." >&2
|
|
752
|
-
echo "Fix: open Forge → Settings → Skills → Connectors tab → GitLab, paste a fresh Personal Access Token, click Save + Test." >&2
|
|
753
|
-
echo "Then retry this node from the pipeline UI — Forge will push the new token to glab automatically." >&2
|
|
754
|
-
fi
|
|
755
|
-
|
|
756
|
-
MR_URL=$(echo "$GLAB_OUT" | grep -oE 'https://[^[:space:]]+/-/merge_requests/[0-9]+' | head -1)
|
|
757
|
-
|
|
758
|
-
# If glab succeeded but we didn't parse a URL (output format
|
|
759
|
-
# variant), OR if the MR already existed (glab errors on conflict),
|
|
760
|
-
# fall back to looking up the MR by source branch.
|
|
761
|
-
if [ -z "$MR_URL" ]; then
|
|
762
|
-
echo "Fallback: looking up existing MR for branch $BRANCH"
|
|
763
|
-
MR_URL=$(glab mr view "$BRANCH" -R "$PROJECT_PATH" --output json 2>/dev/null | jq -r '.web_url // empty')
|
|
764
|
-
if [ -z "$MR_URL" ]; then
|
|
765
|
-
# Last resort: glab mr list with our source branch.
|
|
766
|
-
MR_URL=$(glab mr list -R "$PROJECT_PATH" --source-branch "$BRANCH" --output json 2>/dev/null \\
|
|
767
|
-
| jq -r '.[0].web_url // empty')
|
|
768
|
-
fi
|
|
769
|
-
fi
|
|
770
|
-
|
|
771
|
-
if [ -z "$MR_URL" ]; then
|
|
772
|
-
echo "ERROR: could not create OR find MR for $BRANCH. See glab output above." >&2
|
|
773
|
-
# Don't fail the node hard — push succeeded, the user can create
|
|
774
|
-
# the MR manually via the GitLab "create MR" hint in the push
|
|
775
|
-
# output. notify-teams will see empty MR_URL and skip.
|
|
776
|
-
fi
|
|
777
|
-
|
|
778
|
-
echo "MR_URL=$MR_URL"
|
|
779
|
-
echo "REVIEWER=$REVIEWER_USERNAME"
|
|
780
|
-
outputs:
|
|
781
|
-
- name: mr
|
|
782
|
-
extract: stdout
|
|
783
|
-
notify-teams:
|
|
784
|
-
mode: shell
|
|
785
|
-
project: "{{input.project}}"
|
|
786
|
-
worktree: false
|
|
787
|
-
depends_on: [push-and-mr]
|
|
788
|
-
prompt: |
|
|
789
|
-
set -e
|
|
790
|
-
MR_OUT=$'{{nodes.push-and-mr.outputs.mr}}'
|
|
791
|
-
eval "$(echo "$MR_OUT" | grep -E '^[A-Za-z_][A-Za-z0-9_]*=' | sed 's/^/export /')"
|
|
792
|
-
if [ -z "$MR_URL" ]; then
|
|
793
|
-
echo "SKIP — no MR URL (Claude likely made no changes)"
|
|
794
|
-
exit 0
|
|
795
|
-
fi
|
|
796
|
-
MSG_TPL="{{input.teams_message_template}}"
|
|
797
|
-
[ -z "$MSG_TPL" ] && MSG_TPL="🤖 Mantis #{bug_id} fixed — please review MR: {mr_url}"$'\\n'"Bug: {summary}"
|
|
798
|
-
# Forge exposes /api/connector-tool on loopback (no auth) — pipelines
|
|
799
|
-
# call connector tools through it. teams.send_message takes name (fuzzy
|
|
800
|
-
# match against any chat the user is in) + text.
|
|
801
|
-
send_to() {
|
|
802
|
-
local who="$1"
|
|
803
|
-
local role="$2"
|
|
804
|
-
if [ -z "$who" ]; then echo "SKIP — no $role name"; return 0; fi
|
|
805
|
-
# Render template + build JSON payload in one Python invocation so we
|
|
806
|
-
# never have to worry about shell quoting of summary / description /
|
|
807
|
-
# MR URL when they contain |, $, backticks, etc.
|
|
808
|
-
local payload
|
|
809
|
-
payload=$(python3 - "$MSG_TPL" "{{input.bug_id}}" "{{input.summary}}" "$role" "$MR_URL" "$who" <<'PY'
|
|
810
|
-
import json, sys
|
|
811
|
-
tpl, bug_id, summary, role, mr_url, who = sys.argv[1:7]
|
|
812
|
-
text = (tpl.replace('{bug_id}', bug_id)
|
|
813
|
-
.replace('{summary}', summary)
|
|
814
|
-
.replace('{role}', role)
|
|
815
|
-
.replace('{mr_url}', mr_url))
|
|
816
|
-
print(json.dumps({'plugin_id': 'teams', 'tool': 'send_message',
|
|
817
|
-
'input': {'name': who, 'text': text}}))
|
|
818
|
-
PY
|
|
819
|
-
)
|
|
820
|
-
echo "Sending to Teams chat '$who' ($role)…"
|
|
821
|
-
curl -sS -X POST http://127.0.0.1:8403/api/connector-tool \\
|
|
822
|
-
-H 'content-type: application/json' \\
|
|
823
|
-
--data-binary "$payload" \\
|
|
824
|
-
| jq .
|
|
825
|
-
}
|
|
826
|
-
send_to "{{input.assignee}}" "assignee"
|
|
827
|
-
send_to "{{input.reporter}}" "reporter"
|
|
828
|
-
echo "DONE — MR: $MR_URL"
|
|
829
|
-
outputs:
|
|
830
|
-
- name: result
|
|
831
|
-
extract: stdout
|
|
832
|
-
cleanup:
|
|
833
|
-
mode: shell
|
|
834
|
-
project: "{{input.project}}"
|
|
835
|
-
worktree: false
|
|
836
|
-
depends_on: [notify-teams]
|
|
837
|
-
prompt: |
|
|
838
|
-
# Best-effort cleanup so the project working tree doesn't pile up
|
|
839
|
-
# one full checkout per fixed bug. Runs only after notify-teams
|
|
840
|
-
# succeeded — if anything upstream failed, the worktree is kept
|
|
841
|
-
# for inspection. The remote branch + MR still live on GitLab.
|
|
842
|
-
set +e
|
|
843
|
-
WORKTREE_DIR=$'{{nodes.worktree-setup.outputs.wt}}'
|
|
844
|
-
WORKTREE_DIR=$(echo "$WORKTREE_DIR" | head -1 | tr -d '[:space:]')
|
|
845
|
-
# Derive BRANCH from inside the worktree before we delete it.
|
|
846
|
-
BRANCH=$(git -C "$WORKTREE_DIR" symbolic-ref --short HEAD 2>/dev/null)
|
|
847
|
-
cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
|
848
|
-
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
849
|
-
[ -z "$ROOT" ] && { echo "SKIP — not in a git repo"; exit 0; }
|
|
850
|
-
# Paranoia — only clean under our .forge/worktrees/.
|
|
851
|
-
case "$WORKTREE_DIR" in
|
|
852
|
-
"$ROOT/.forge/worktrees/"*) : ;;
|
|
853
|
-
*) echo "SKIP — WORKTREE_DIR=$WORKTREE_DIR not under .forge/worktrees/"; exit 0 ;;
|
|
854
|
-
esac
|
|
855
|
-
echo "removing worktree $WORKTREE_DIR …"
|
|
856
|
-
git worktree remove --force "$WORKTREE_DIR" 2>&1 || rm -rf "$WORKTREE_DIR"
|
|
857
|
-
git worktree prune
|
|
858
|
-
# Drop the local branch too — remote copy is on GitLab + the MR.
|
|
859
|
-
[ -n "$BRANCH" ] && git branch -D "$BRANCH" 2>/dev/null && echo "deleted local branch $BRANCH"
|
|
860
|
-
echo "cleanup done"
|
|
861
|
-
outputs:
|
|
862
|
-
- name: result
|
|
863
|
-
extract: stdout
|
|
864
255
|
`,
|
|
865
256
|
'multi-agent-collaboration': `
|
|
866
257
|
name: multi-agent-collaboration
|
|
@@ -2070,7 +1461,11 @@ async function scheduleReadyNodes(pipeline: Pipeline, workflow: Workflow) {
|
|
|
2070
1461
|
// Shell steps receive env vars: FORGE_WORKTREE, FORGE_WORKTREE_BRANCH, FORGE_PROJECT_ROOT.
|
|
2071
1462
|
// Set worktree: false on a node to skip (e.g. read-only gh commands that don't need isolation).
|
|
2072
1463
|
let effectivePath = projectInfo.path;
|
|
2073
|
-
|
|
1464
|
+
// `workdir:` always wins (sets effectivePath at the bottom). If it's
|
|
1465
|
+
// declared, skip the auto-worktree creation entirely — otherwise we
|
|
1466
|
+
// produce dead `.forge/worktrees/pipeline-<id>` directories that
|
|
1467
|
+
// nothing ever uses but pile up on disk per pipeline run.
|
|
1468
|
+
const useWorktree = nodeDef.worktree !== false && !nodeDef.workdir;
|
|
2074
1469
|
const branchName = nodeDef.branch ? resolveTemplate(nodeDef.branch, ctx) : `pipeline/${pipeline.id.slice(0, 8)}`;
|
|
2075
1470
|
if (useWorktree) try {
|
|
2076
1471
|
const { execSync } = require('node:child_process');
|
package/lib/settings.ts
CHANGED
|
@@ -71,6 +71,11 @@ export interface Settings {
|
|
|
71
71
|
* (offline only). Default: `aiwatching/forge-connectors`.
|
|
72
72
|
*/
|
|
73
73
|
connectorsRepoUrl: string;
|
|
74
|
+
/**
|
|
75
|
+
* Remote registry for workflow templates (recipes + pipelines). Same
|
|
76
|
+
* shape as connectorsRepoUrl. Default: `aiwatching/forge-workflow`.
|
|
77
|
+
*/
|
|
78
|
+
workflowRepoUrl: string;
|
|
74
79
|
displayName: string;
|
|
75
80
|
displayEmail: string;
|
|
76
81
|
favoriteProjects: string[];
|
|
@@ -126,6 +131,7 @@ const defaults: Settings = {
|
|
|
126
131
|
notificationRetentionDays: 30,
|
|
127
132
|
skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
|
|
128
133
|
connectorsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-connectors/main',
|
|
134
|
+
workflowRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-workflow/main',
|
|
129
135
|
displayName: 'Forge',
|
|
130
136
|
displayEmail: '',
|
|
131
137
|
favoriteProjects: [],
|