@plainconceptsplatform/workflows 1.2.3 → 1.3.1
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/dist/route-processing.js +1 -0
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +108 -108
- package/loops/actions/assess-blast-radius/action.yml +47 -8
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +27 -5
- package/loops/actions/assess-blast-radius/migration-is-additive.sh +38 -0
- package/loops/actions/classify-route/action.yml +2 -1
- package/loops/actions/classify-route/classify-route.sh +7 -1
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +52 -52
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +6 -1
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +9 -2
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +149 -29
- package/loops/docs/diagrams.md +26 -11
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +5 -0
- package/loops/workflows/agent-implement.md +6 -0
- package/loops/workflows/agent-merge-gate.md +100 -3
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/evidence.yml +79 -4
- package/loops/workflows/shared/platform-defaults.md +23 -23
- package/loops/workflows/work-router.yml +97 -21
- package/package.json +1 -1
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
-
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
-
#
|
|
5
|
-
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
-
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
-
name: "Authorize Bot Work"
|
|
8
|
-
|
|
9
|
-
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
-
|
|
11
|
-
on:
|
|
12
|
-
issues:
|
|
13
|
-
types: [labeled]
|
|
14
|
-
|
|
15
|
-
permissions:
|
|
16
|
-
contents: read
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
authorize:
|
|
20
|
-
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
-
# already own the issue.
|
|
22
|
-
#
|
|
23
|
-
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
-
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
-
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
-
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
-
#
|
|
28
|
-
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
-
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
-
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
-
# bot-working is added `review` is already gone.
|
|
32
|
-
if: >
|
|
33
|
-
(github.event.label.name == 'implement' ||
|
|
34
|
-
github.event.label.name == 'refine') &&
|
|
35
|
-
!contains(github.event.issue.labels.*.name, 'bot-working') &&
|
|
36
|
-
!endsWith(github.actor, '[bot]')
|
|
37
|
-
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
38
|
-
timeout-minutes: 5
|
|
39
|
-
concurrency:
|
|
40
|
-
group: authorize-${{ github.event.issue.number }}
|
|
41
|
-
cancel-in-progress: false
|
|
42
|
-
permissions:
|
|
43
|
-
contents: read
|
|
44
|
-
issues: write
|
|
45
|
-
steps:
|
|
46
|
-
- name: Check actor permission
|
|
47
|
-
id: check
|
|
48
|
-
env:
|
|
49
|
-
GH_TOKEN: ${{ github.token }}
|
|
50
|
-
ACTOR: ${{ github.actor }}
|
|
51
|
-
run: |
|
|
52
|
-
set -euo pipefail
|
|
53
|
-
|
|
54
|
-
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" \
|
|
55
|
-
--jq '.permission' 2>/dev/null || echo "none")
|
|
56
|
-
|
|
57
|
-
case "$permission" in
|
|
58
|
-
admin | maintain | write)
|
|
59
|
-
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
|
60
|
-
echo "::notice::$ACTOR has $permission permission - authorized"
|
|
61
|
-
;;
|
|
62
|
-
*)
|
|
63
|
-
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
|
64
|
-
echo "::warning::$ACTOR has '$permission' permission - not authorized"
|
|
65
|
-
;;
|
|
66
|
-
esac
|
|
67
|
-
|
|
68
|
-
- name: Checkout for App token action
|
|
69
|
-
if: steps.check.outputs.authorized == 'true'
|
|
70
|
-
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
71
|
-
with:
|
|
72
|
-
persist-credentials: false
|
|
73
|
-
|
|
74
|
-
- name: Generate App token
|
|
75
|
-
if: steps.check.outputs.authorized == 'true'
|
|
76
|
-
id: app-token
|
|
77
|
-
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
78
|
-
with:
|
|
79
|
-
app-id: ${{ secrets.BOT_APP_ID }}
|
|
80
|
-
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
81
|
-
|
|
82
|
-
- name: Bot takes the issue over
|
|
83
|
-
if: steps.check.outputs.authorized == 'true'
|
|
84
|
-
env:
|
|
85
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
86
|
-
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
87
|
-
LABEL: ${{ github.event.label.name }}
|
|
88
|
-
run: |
|
|
89
|
-
set -euo pipefail
|
|
90
|
-
|
|
91
|
-
# Clear review FIRST. A person adding a work label is the human review the label was
|
|
92
|
-
# waiting for, and the classifier refuses to route an issue that still carries it, so
|
|
93
|
-
# removing it here is what makes the hand-off work at all. It also has to happen
|
|
94
|
-
# before bot-working, because that is the event the classifier reads: adding
|
|
95
|
-
# bot-working first would raise an event whose payload still shows review.
|
|
96
|
-
# --remove-label on an absent label is a no-op, so this is safe on a fresh issue.
|
|
97
|
-
# `stalled` goes with it: a person taking the issue on restarts the work, so the
|
|
98
|
-
# marker the janitor retries on must not survive into the new run.
|
|
99
|
-
gh issue edit "$ISSUE_NUMBER" --remove-label "review" --remove-label "stalled" 2>/dev/null ||
|
|
100
|
-
echo "::notice::no review or stalled label to clear on #$ISSUE_NUMBER"
|
|
101
|
-
|
|
102
|
-
echo "Adding bot-working label to issue #$ISSUE_NUMBER (triggered by $LABEL)"
|
|
103
|
-
gh issue edit "$ISSUE_NUMBER" --add-label "bot-working"
|
|
104
|
-
|
|
105
|
-
echo "::notice::bot-working label added - bot will now own the $LABEL workflow"
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
+
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
+
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
+
name: "Authorize Bot Work"
|
|
8
|
+
|
|
9
|
+
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
issues:
|
|
13
|
+
types: [labeled]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
authorize:
|
|
20
|
+
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
+
# already own the issue.
|
|
22
|
+
#
|
|
23
|
+
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
+
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
+
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
+
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
+
#
|
|
28
|
+
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
+
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
+
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
+
# bot-working is added `review` is already gone.
|
|
32
|
+
if: >
|
|
33
|
+
(github.event.label.name == 'implement' ||
|
|
34
|
+
github.event.label.name == 'refine') &&
|
|
35
|
+
!contains(github.event.issue.labels.*.name, 'bot-working') &&
|
|
36
|
+
!endsWith(github.actor, '[bot]')
|
|
37
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
38
|
+
timeout-minutes: 5
|
|
39
|
+
concurrency:
|
|
40
|
+
group: authorize-${{ github.event.issue.number }}
|
|
41
|
+
cancel-in-progress: false
|
|
42
|
+
permissions:
|
|
43
|
+
contents: read
|
|
44
|
+
issues: write
|
|
45
|
+
steps:
|
|
46
|
+
- name: Check actor permission
|
|
47
|
+
id: check
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ github.token }}
|
|
50
|
+
ACTOR: ${{ github.actor }}
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
|
|
54
|
+
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" \
|
|
55
|
+
--jq '.permission' 2>/dev/null || echo "none")
|
|
56
|
+
|
|
57
|
+
case "$permission" in
|
|
58
|
+
admin | maintain | write)
|
|
59
|
+
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
|
60
|
+
echo "::notice::$ACTOR has $permission permission - authorized"
|
|
61
|
+
;;
|
|
62
|
+
*)
|
|
63
|
+
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
|
64
|
+
echo "::warning::$ACTOR has '$permission' permission - not authorized"
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
- name: Checkout for App token action
|
|
69
|
+
if: steps.check.outputs.authorized == 'true'
|
|
70
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
71
|
+
with:
|
|
72
|
+
persist-credentials: false
|
|
73
|
+
|
|
74
|
+
- name: Generate App token
|
|
75
|
+
if: steps.check.outputs.authorized == 'true'
|
|
76
|
+
id: app-token
|
|
77
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
78
|
+
with:
|
|
79
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
80
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
81
|
+
|
|
82
|
+
- name: Bot takes the issue over
|
|
83
|
+
if: steps.check.outputs.authorized == 'true'
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
86
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
87
|
+
LABEL: ${{ github.event.label.name }}
|
|
88
|
+
run: |
|
|
89
|
+
set -euo pipefail
|
|
90
|
+
|
|
91
|
+
# Clear review FIRST. A person adding a work label is the human review the label was
|
|
92
|
+
# waiting for, and the classifier refuses to route an issue that still carries it, so
|
|
93
|
+
# removing it here is what makes the hand-off work at all. It also has to happen
|
|
94
|
+
# before bot-working, because that is the event the classifier reads: adding
|
|
95
|
+
# bot-working first would raise an event whose payload still shows review.
|
|
96
|
+
# --remove-label on an absent label is a no-op, so this is safe on a fresh issue.
|
|
97
|
+
# `stalled` goes with it: a person taking the issue on restarts the work, so the
|
|
98
|
+
# marker the janitor retries on must not survive into the new run.
|
|
99
|
+
gh issue edit "$ISSUE_NUMBER" --remove-label "review" --remove-label "stalled" 2>/dev/null ||
|
|
100
|
+
echo "::notice::no review or stalled label to clear on #$ISSUE_NUMBER"
|
|
101
|
+
|
|
102
|
+
echo "Adding bot-working label to issue #$ISSUE_NUMBER (triggered by $LABEL)"
|
|
103
|
+
gh issue edit "$ISSUE_NUMBER" --add-label "bot-working"
|
|
104
|
+
|
|
105
|
+
echo "::notice::bot-working label added - bot will now own the $LABEL workflow"
|
|
@@ -38,9 +38,11 @@ env:
|
|
|
38
38
|
# The SDK when a repository has .NET projects but no global.json to say which.
|
|
39
39
|
EVIDENCE_DOTNET_VERSION: "10.0.x"
|
|
40
40
|
EVIDENCE_MARKER: "<!-- agentic-evidence -->"
|
|
41
|
-
# How long the run keeps the screenshots it could not
|
|
42
|
-
# has no plan on the default branch to keep them beside.
|
|
41
|
+
# How long the run keeps the screenshots it could not commit anywhere.
|
|
43
42
|
EVIDENCE_ARTIFACT_DAYS: "14"
|
|
43
|
+
# Where an unmerged pull request's screenshots go, under pr-<number>/: its plans are not on the
|
|
44
|
+
# default branch yet, and a commit to its own branch would invalidate the gate's verdict.
|
|
45
|
+
EVIDENCE_PREVIEW_BRANCH: "evidence-previews"
|
|
44
46
|
# The capture runs in mcr.microsoft.com/playwright at this tag, pre-pulled by the runner image
|
|
45
47
|
# (runners/versions.env), so the two move together.
|
|
46
48
|
PLAYWRIGHT_VERSION: "1.62.1"
|
|
@@ -393,6 +395,72 @@ jobs:
|
|
|
393
395
|
} >> "$GITHUB_OUTPUT"
|
|
394
396
|
echo "Screenshots kept at ${SERVER}/${REPO}/tree/${sha} beside $(( ${#dirs[@]} - ${#missing[@]} )) plan(s)"
|
|
395
397
|
|
|
398
|
+
# The reviewer of a parked pull request needs the screenshots before the merge. One orphan
|
|
399
|
+
# branch holds every preview, a folder per pull request; GITHUB_TOKEN pushes start no workflow.
|
|
400
|
+
- name: Commit a preview of the screenshots
|
|
401
|
+
id: preview
|
|
402
|
+
if: steps.pr.outputs.merged != 'true'
|
|
403
|
+
env:
|
|
404
|
+
TOKEN: ${{ github.token }}
|
|
405
|
+
REPO: ${{ github.repository }}
|
|
406
|
+
SERVER: ${{ github.server_url }}
|
|
407
|
+
PREVIEW: ${{ env.EVIDENCE_PREVIEW_BRANCH }}
|
|
408
|
+
PR: ${{ inputs.pr-number }}
|
|
409
|
+
run: |
|
|
410
|
+
set -euo pipefail
|
|
411
|
+
src="$RUNNER_TEMP/evidence/files"
|
|
412
|
+
if [ ! -d "$src" ] || [ -z "$(find "$src" -name '*.jpg' -print -quit)" ]; then
|
|
413
|
+
echo "base=" >> "$GITHUB_OUTPUT"
|
|
414
|
+
exit 0
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
work="$RUNNER_TEMP/evidence-preview"
|
|
418
|
+
rm -rf "$work"
|
|
419
|
+
mkdir -p "$work"
|
|
420
|
+
cd "$work"
|
|
421
|
+
auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)"
|
|
422
|
+
git init -q
|
|
423
|
+
git config "http.${SERVER}/.extraheader" "$auth"
|
|
424
|
+
git config user.name "github-actions[bot]"
|
|
425
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
426
|
+
git config core.sparseCheckout true
|
|
427
|
+
echo "/pr-${PR}/" > .git/info/sparse-checkout
|
|
428
|
+
git remote add origin "${SERVER}/${REPO}.git"
|
|
429
|
+
|
|
430
|
+
pushed=false
|
|
431
|
+
for attempt in 1 2 3 4 5; do
|
|
432
|
+
if git fetch -q --depth=1 --filter=blob:none origin "refs/heads/$PREVIEW" 2>/dev/null; then
|
|
433
|
+
git checkout -q -f -B "$PREVIEW" FETCH_HEAD
|
|
434
|
+
else
|
|
435
|
+
# No branch yet: start a fresh orphan each attempt, so a failed push is never
|
|
436
|
+
# mistaken for one that landed.
|
|
437
|
+
git checkout -q --orphan "preview-$attempt"
|
|
438
|
+
git rm -rq --cached . >/dev/null 2>&1 || true
|
|
439
|
+
fi
|
|
440
|
+
rm -rf "pr-${PR}"
|
|
441
|
+
mkdir -p "pr-${PR}"
|
|
442
|
+
cp -R "$src/." "pr-${PR}/"
|
|
443
|
+
git add -A "pr-${PR}"
|
|
444
|
+
if git diff --cached --quiet; then
|
|
445
|
+
pushed=true
|
|
446
|
+
break
|
|
447
|
+
fi
|
|
448
|
+
git commit -q -m "evidence: preview for #$PR [skip ci]"
|
|
449
|
+
if git push -q origin "HEAD:refs/heads/$PREVIEW"; then
|
|
450
|
+
pushed=true
|
|
451
|
+
break
|
|
452
|
+
fi
|
|
453
|
+
echo "$PREVIEW moved before the push (attempt $attempt); retrying on its new tip."
|
|
454
|
+
sleep $((attempt * 3))
|
|
455
|
+
done
|
|
456
|
+
if [ "$pushed" != "true" ]; then
|
|
457
|
+
echo "::warning::could not push the preview to $PREVIEW after 5 attempts"
|
|
458
|
+
echo "base=" >> "$GITHUB_OUTPUT"
|
|
459
|
+
exit 0
|
|
460
|
+
fi
|
|
461
|
+
sha="$(git rev-parse HEAD)"
|
|
462
|
+
echo "base=${SERVER}/${REPO}/raw/${sha}/pr-${PR}" >> "$GITHUB_OUTPUT"
|
|
463
|
+
|
|
396
464
|
# GITHUB_TOKEN on purpose: its comment raises no workflow event, so the router does not wake
|
|
397
465
|
# up to read its own record. One comment per pull request, rewritten on a re-run.
|
|
398
466
|
- name: Write the evidence comment
|
|
@@ -401,7 +469,9 @@ jobs:
|
|
|
401
469
|
PR_NUMBER: ${{ inputs.pr-number }}
|
|
402
470
|
MARKER: ${{ env.EVIDENCE_MARKER }}
|
|
403
471
|
MERGED: ${{ steps.pr.outputs.merged }}
|
|
404
|
-
IMAGE_BASE: ${{ steps.archive.outputs.base }}
|
|
472
|
+
IMAGE_BASE: ${{ steps.archive.outputs.base || steps.preview.outputs.base }}
|
|
473
|
+
PREVIEW: ${{ steps.preview.outputs.base != '' && 'true' || 'false' }}
|
|
474
|
+
PREVIEW_BRANCH: ${{ env.EVIDENCE_PREVIEW_BRANCH }}
|
|
405
475
|
MISSING: ${{ steps.archive.outputs.missing }}
|
|
406
476
|
REASON: ${{ steps.archive.outputs.reason }}
|
|
407
477
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
@@ -421,7 +491,12 @@ jobs:
|
|
|
421
491
|
// the run's artifact has it instead.
|
|
422
492
|
source = source.replace(/^!\[[^\]]*\]\(__EVIDENCE_BASE__\/([^)]*)\)\n?/gm, (line, file) => (kept(file) ? line : ''));
|
|
423
493
|
const shots = fs.readdirSync(evidence, { recursive: true }).some((name) => String(name).endsWith('.jpg'));
|
|
424
|
-
if (
|
|
494
|
+
if (process.env.PREVIEW === 'true') {
|
|
495
|
+
source = source.replace(/^The screenshots are kept beside each plan.*$/m,
|
|
496
|
+
`A preview before the merge: the screenshots are on the \`${process.env.PREVIEW_BRANCH}\` branch for now, and are kept beside each plan once the pull request merges.`);
|
|
497
|
+
} else if (!base) {
|
|
498
|
+
source = source.replace(/^The screenshots are kept beside each plan.*\n\n?/m, '');
|
|
499
|
+
}
|
|
425
500
|
if (shots && (!base || missing.length > 0)) {
|
|
426
501
|
const why = process.env.MERGED !== 'true'
|
|
427
502
|
? 'the pull request is not merged, so its plans are not on the default branch'
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
---
|
|
2
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/platform-defaults.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
description: Shared network and safe-output defaults for catalog agent workflows, plus the verification rules every worker that builds or pushes has to follow. The body below is runtime-imported into each worker's prompt ahead of its own steps.
|
|
4
|
-
|
|
5
|
-
network:
|
|
6
|
-
allowed:
|
|
7
|
-
- defaults
|
|
8
|
-
- forge.plainconcepts.com
|
|
9
|
-
# opencode fetches its model registry from models.opencode.ai at startup; without
|
|
10
|
-
# this host on the allowlist the fetch blocks bootstrap and the run stalls after
|
|
11
|
-
# "init" until the runner kills it. The apex "opencode.ai" is not enough.
|
|
12
|
-
- models.opencode.ai
|
|
13
|
-
- node
|
|
14
|
-
- github
|
|
15
|
-
- dotnet
|
|
16
|
-
- fonts
|
|
17
|
-
- login.microsoftonline.com
|
|
18
|
-
# centralised AgentMemory (App Service); the MCP shim needs it through the egress proxy
|
|
19
|
-
- agentmemory-pro-01.azurewebsites.net
|
|
20
|
-
|
|
21
|
-
safe-outputs:
|
|
22
|
-
threat-detection: false
|
|
23
|
-
---
|
|
1
|
+
---
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/platform-defaults.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
description: Shared network and safe-output defaults for catalog agent workflows, plus the verification rules every worker that builds or pushes has to follow. The body below is runtime-imported into each worker's prompt ahead of its own steps.
|
|
4
|
+
|
|
5
|
+
network:
|
|
6
|
+
allowed:
|
|
7
|
+
- defaults
|
|
8
|
+
- forge.plainconcepts.com
|
|
9
|
+
# opencode fetches its model registry from models.opencode.ai at startup; without
|
|
10
|
+
# this host on the allowlist the fetch blocks bootstrap and the run stalls after
|
|
11
|
+
# "init" until the runner kills it. The apex "opencode.ai" is not enough.
|
|
12
|
+
- models.opencode.ai
|
|
13
|
+
- node
|
|
14
|
+
- github
|
|
15
|
+
- dotnet
|
|
16
|
+
- fonts
|
|
17
|
+
- login.microsoftonline.com
|
|
18
|
+
# centralised AgentMemory (App Service); the MCP shim needs it through the egress proxy
|
|
19
|
+
- agentmemory-pro-01.azurewebsites.net
|
|
20
|
+
|
|
21
|
+
safe-outputs:
|
|
22
|
+
threat-detection: false
|
|
23
|
+
---
|
|
24
24
|
|
|
25
25
|
## Verification, for every worker that builds or pushes
|
|
26
26
|
|
|
@@ -562,12 +562,9 @@ jobs:
|
|
|
562
562
|
# only changes between gates, every merge re-arms the siblings through
|
|
563
563
|
# detect-pr-conflicts's rescan, and the queue drains one pull request at a time.
|
|
564
564
|
#
|
|
565
|
-
# `
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
# one sibling per merge. It is a trap for a person dispatching by hand: labelling four pull
|
|
569
|
-
# requests `merge-gate` in one go leaves one running, one pending and two silently cancelled,
|
|
570
|
-
# with nothing on the pull requests to say so. Dispatch one, let it finish, then the next.
|
|
565
|
+
# `queue: max` keeps up to 100 gates waiting in order. Without it GitHub keeps one pending run
|
|
566
|
+
# per group and silently cancels the previous one: in one week Numa cancelled 12 gates and
|
|
567
|
+
# finished 6, and those pull requests were merged by hand without a verdict.
|
|
571
568
|
call-merge-gate:
|
|
572
569
|
needs: classify
|
|
573
570
|
if: needs.classify.outputs.route == 'merge-gate'
|
|
@@ -575,6 +572,7 @@ jobs:
|
|
|
575
572
|
concurrency:
|
|
576
573
|
group: merge-belt
|
|
577
574
|
cancel-in-progress: false
|
|
575
|
+
queue: max
|
|
578
576
|
permissions: write-all
|
|
579
577
|
with:
|
|
580
578
|
pr-number: ${{ needs.classify.outputs.pr-number }}
|
|
@@ -590,6 +588,94 @@ jobs:
|
|
|
590
588
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
591
589
|
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
592
590
|
|
|
591
|
+
# The decision the gate parked for: a person with write access approves a bot pull request the
|
|
592
|
+
# gate handed to them (human-review or owner-review), and the bot merges the commit they
|
|
593
|
+
# approved. Re-running the gate would only reproduce the same parked verdict.
|
|
594
|
+
merge-approved:
|
|
595
|
+
needs: classify
|
|
596
|
+
if: needs.classify.outputs.route == 'merge-approved'
|
|
597
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
598
|
+
timeout-minutes: 5
|
|
599
|
+
concurrency:
|
|
600
|
+
group: merge-approved-${{ needs.classify.outputs.pr-number }}
|
|
601
|
+
cancel-in-progress: false
|
|
602
|
+
permissions:
|
|
603
|
+
contents: read
|
|
604
|
+
pull-requests: write
|
|
605
|
+
steps:
|
|
606
|
+
- name: Checkout for App token action
|
|
607
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
608
|
+
with:
|
|
609
|
+
persist-credentials: false
|
|
610
|
+
- name: Generate App token
|
|
611
|
+
id: app-token
|
|
612
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
613
|
+
with:
|
|
614
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
615
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
616
|
+
- name: Merge the approved commit
|
|
617
|
+
id: merge
|
|
618
|
+
env:
|
|
619
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
620
|
+
REPO: ${{ github.repository }}
|
|
621
|
+
PR: ${{ needs.classify.outputs.pr-number }}
|
|
622
|
+
ISSUE: ${{ needs.classify.outputs.linked-issue }}
|
|
623
|
+
REVIEWER: ${{ github.event.review.user.login }}
|
|
624
|
+
REVIEW_COMMIT: ${{ github.event.review.commit_id }}
|
|
625
|
+
run: |
|
|
626
|
+
set -euo pipefail
|
|
627
|
+
skip() { echo "::notice::Not merging #$PR: $1"; exit 0; }
|
|
628
|
+
|
|
629
|
+
permission=$(gh api "repos/$REPO/collaborators/$REVIEWER/permission" --jq '.permission' 2>/dev/null || echo none)
|
|
630
|
+
case "$permission" in admin|maintain|write) ;; *) skip "$REVIEWER has $permission access" ;; esac
|
|
631
|
+
|
|
632
|
+
read -r state author head_sha < <(gh api "repos/$REPO/pulls/$PR" --jq '[.state, .user.login, .head.sha] | @tsv')
|
|
633
|
+
[ "$state" = open ] || skip "it is $state"
|
|
634
|
+
case " $TRUSTED_BOTS " in *" $author "*) ;; *) skip "it was opened by $author, not the bot" ;; esac
|
|
635
|
+
[ "$head_sha" = "$REVIEW_COMMIT" ] || skip "$REVIEWER approved ${REVIEW_COMMIT:0:7}, the head is now ${head_sha:0:7}"
|
|
636
|
+
|
|
637
|
+
# Another reviewer's change request still stands.
|
|
638
|
+
decision=$(gh pr view "$PR" --repo "$REPO" --json reviewDecision --jq '.reviewDecision // ""')
|
|
639
|
+
[ "$decision" = APPROVED ] || skip "the review decision is '${decision:-none}'"
|
|
640
|
+
|
|
641
|
+
# The gate parked it for a person. A protected-path hold comments on the issue rather
|
|
642
|
+
# than the pull request, so the issue's labels speak for it.
|
|
643
|
+
verdict=$(gh api --paginate "repos/$REPO/issues/$PR/comments?per_page=100" \
|
|
644
|
+
--jq '.[] | select((.body | contains("<!-- agent-merge-gate -->")) and (.body | contains("**Verdict:**"))) | .body' |
|
|
645
|
+
grep -oE '\*\*Verdict:\*\* [a-z-]+' | tail -1 | sed 's/.* //' || true)
|
|
646
|
+
if [ -z "$verdict" ] && [ -n "$ISSUE" ]; then
|
|
647
|
+
labels=$(gh api "repos/$REPO/issues/$ISSUE/labels" --jq '[.[].name] | join(" ")')
|
|
648
|
+
case " $labels " in *" blocked "*) ;; *" owner-review "*) verdict=owner-review ;; esac
|
|
649
|
+
fi
|
|
650
|
+
case "$verdict" in human-review|owner-review) ;; *) skip "the gate's verdict is '${verdict:-none}', not a hand-over" ;; esac
|
|
651
|
+
|
|
652
|
+
ci=$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" \
|
|
653
|
+
--jq '[.workflow_runs[] | select(.name == env.CI_WORKFLOW_NAME)] | sort_by(.created_at) | last | .conclusion // ""')
|
|
654
|
+
[ "$ci" = success ] || skip "$CI_WORKFLOW_NAME on ${head_sha:0:7} is '${ci:-not run}'"
|
|
655
|
+
|
|
656
|
+
gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
|
|
657
|
+
echo "merged=true" >> "$GITHUB_OUTPUT"
|
|
658
|
+
- name: Clear the issue's labels
|
|
659
|
+
if: steps.merge.outputs.merged == 'true' && needs.classify.outputs.linked-issue != ''
|
|
660
|
+
env:
|
|
661
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
662
|
+
REPO: ${{ github.repository }}
|
|
663
|
+
ISSUE: ${{ needs.classify.outputs.linked-issue }}
|
|
664
|
+
run: |
|
|
665
|
+
for label in implement bot-working review owner-review blocked pr-pending; do
|
|
666
|
+
gh api -X DELETE "repos/$REPO/issues/$ISSUE/labels/$label" >/dev/null 2>&1 || true
|
|
667
|
+
done
|
|
668
|
+
# GITHUB_TOKEN: its comment raises no event for the router to read.
|
|
669
|
+
- name: Say whose approval merged it
|
|
670
|
+
if: steps.merge.outputs.merged == 'true'
|
|
671
|
+
env:
|
|
672
|
+
GH_TOKEN: ${{ github.token }}
|
|
673
|
+
REPO: ${{ github.repository }}
|
|
674
|
+
PR: ${{ needs.classify.outputs.pr-number }}
|
|
675
|
+
REVIEWER: ${{ github.event.review.user.login }}
|
|
676
|
+
REVIEW_COMMIT: ${{ github.event.review.commit_id }}
|
|
677
|
+
run: gh pr comment "$PR" --repo "$REPO" --body "Merged on @$REVIEWER's approval of \`${REVIEW_COMMIT:0:7}\`."
|
|
678
|
+
|
|
593
679
|
# Screenshots of a merged pull request, replayed from the verification plan its implementation
|
|
594
680
|
# wrote and archived beside that plan. Plain YAML with no model, so no secret is passed and the
|
|
595
681
|
# grant is exactly what the publish job writes: one commit of evidence/ folders and one comment. Its capture
|
|
@@ -751,7 +837,7 @@ jobs:
|
|
|
751
837
|
}
|
|
752
838
|
|
|
753
839
|
# A gate already queued or running for this pull request will read the same CI verdict;
|
|
754
|
-
# a second dispatch
|
|
840
|
+
# a second dispatch would wait in the belt's queue and run the same gate twice. Same
|
|
755
841
|
# live-run test as the reconcile belt's stale-reservation sweep, keyed on the exact
|
|
756
842
|
# title run-name gives a gate dispatch.
|
|
757
843
|
gate_is_live() {
|
|
@@ -942,7 +1028,7 @@ jobs:
|
|
|
942
1028
|
}
|
|
943
1029
|
|
|
944
1030
|
# A gate already queued or running for this pull request will read the same CI verdict;
|
|
945
|
-
# a second dispatch
|
|
1031
|
+
# a second dispatch would wait in the belt's queue and run the same gate twice.
|
|
946
1032
|
gate_is_live() {
|
|
947
1033
|
local pr="$1" status live
|
|
948
1034
|
for status in queued in_progress waiting pending; do
|
|
@@ -1103,19 +1189,9 @@ jobs:
|
|
|
1103
1189
|
-f "inputs[ci-run-id]=$ci_run_id" \
|
|
1104
1190
|
-f "inputs[attempts_so_far]=$attempts"
|
|
1105
1191
|
|
|
1106
|
-
#
|
|
1107
|
-
#
|
|
1108
|
-
#
|
|
1109
|
-
# that has already started. A tick that dispatched three gates therefore ran the
|
|
1110
|
-
# last and silently cancelled the other two: no comment, no attempt recorded, and
|
|
1111
|
-
# nothing on the pull request to say it had been passed over. Seen on Pliny-Bot,
|
|
1112
|
-
# where dispatching a gate for PR #135 cancelled the one queued for PR #136.
|
|
1113
|
-
#
|
|
1114
|
-
# Stopping here loses nothing: every pull request eligible this tick is still
|
|
1115
|
-
# eligible next tick, and a run that never started consumes no attempt. The belt
|
|
1116
|
-
# now does one thing and says so, instead of appearing to start work it discarded.
|
|
1117
|
-
echo "One gate dispatched this tick; any other eligible pull request waits for the next one."
|
|
1118
|
-
break
|
|
1192
|
+
# Every eligible pull request in one tick. The belt queues them first in, first out
|
|
1193
|
+
# (`queue: max` on call-merge-gate); before it did, each dispatch cancelled the one
|
|
1194
|
+
# pending ahead of it, so this loop stopped after one and the rest waited an hour.
|
|
1119
1195
|
done
|
|
1120
1196
|
|
|
1121
1197
|
audit-close:
|