@plainconceptsplatform/workflows 0.17.0 → 0.20.0
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/catalog-installation.js +25 -13
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +24 -21
- 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 +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- 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/housekeeping/action.yml +55 -2
- package/loops/actions/identify-gate-subject/action.yml +134 -122
- 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/require-open-issue/action.yml +59 -0
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- 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-refine-output/verify-refine-output.sh +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +630 -8
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +33 -3
- package/loops/workflows/agent-audit.md +36 -0
- package/loops/workflows/agent-implement.md +77 -5
- package/loops/workflows/agent-merge-gate.md +368 -148
- package/loops/workflows/agent-refine.md +93 -17
- package/loops/workflows/agent-release.md +4 -4
- package/loops/workflows/agent-triage.md +33 -1
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/loops/workflows/work-router.yml +25 -11
- package/package.json +2 -2
|
@@ -24,6 +24,7 @@ env:
|
|
|
24
24
|
# re-running a decision produces the same decision. Created idempotently where it is applied.
|
|
25
25
|
STALLED_LABEL: stalled
|
|
26
26
|
REFINE_MARKER: "<!-- agent-refine -->"
|
|
27
|
+
DRAFT_MARKER: "<!-- agent-refine-draft -->"
|
|
27
28
|
INITIAL_MODE: first
|
|
28
29
|
RESPONSE_MODE: rerefine
|
|
29
30
|
MAX_SELF_QUESTIONS: "5"
|
|
@@ -46,6 +47,10 @@ description: |
|
|
|
46
47
|
Refines an issue into a user story, on a first pass or after the author has answered the
|
|
47
48
|
bot's questions. Replaces .loops/recipes/refine-loop.yaml.
|
|
48
49
|
|
|
50
|
+
The refined story is wrapped in the repository's own issue template when one matches.
|
|
51
|
+
When questions remain, the worker leaves that template-wrapped temporal draft on the
|
|
52
|
+
issue and asks every remaining question in a single batched comment.
|
|
53
|
+
|
|
49
54
|
Before writing the story, the agent explores the codebase per work unit (each bullet in a
|
|
50
55
|
bullet-list issue is its own unit), answering its own questions where the code can and
|
|
51
56
|
escalating only genuine business decisions to the author.
|
|
@@ -76,9 +81,41 @@ on:
|
|
|
76
81
|
required: false
|
|
77
82
|
type: string
|
|
78
83
|
default: first
|
|
84
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
85
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
86
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
87
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
88
|
+
# uses for protected_changes.
|
|
89
|
+
needs: [still_open]
|
|
79
90
|
|
|
80
91
|
jobs:
|
|
92
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
93
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
94
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
95
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
96
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
97
|
+
# and gate both the reservation and the agent on it.
|
|
98
|
+
still_open:
|
|
99
|
+
runs-on: agents-arc
|
|
100
|
+
permissions:
|
|
101
|
+
contents: read
|
|
102
|
+
issues: read
|
|
103
|
+
outputs:
|
|
104
|
+
open: ${{ steps.state.outputs.open }}
|
|
105
|
+
steps:
|
|
106
|
+
- name: Checkout workflow actions
|
|
107
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
108
|
+
with:
|
|
109
|
+
persist-credentials: false
|
|
110
|
+
- name: Read the issue state
|
|
111
|
+
id: state
|
|
112
|
+
uses: ./.github/actions/require-open-issue
|
|
113
|
+
with:
|
|
114
|
+
token: ${{ github.token }}
|
|
115
|
+
issue-number: ${{ inputs.issue-number }}
|
|
81
116
|
reserve:
|
|
117
|
+
needs: [still_open]
|
|
118
|
+
if: needs.still_open.outputs.open == 'true'
|
|
82
119
|
runs-on: agents-arc
|
|
83
120
|
permissions:
|
|
84
121
|
contents: read
|
|
@@ -141,6 +178,7 @@ jobs:
|
|
|
141
178
|
with:
|
|
142
179
|
output-file: ${{ steps.output.outputs.output-file }}
|
|
143
180
|
marker: ${{ env.REFINE_MARKER }}
|
|
181
|
+
draft-marker: ${{ env.DRAFT_MARKER }}
|
|
144
182
|
comment-prefix: ${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}
|
|
145
183
|
issue-number: ${{ inputs.issue-number }}
|
|
146
184
|
conclude:
|
|
@@ -364,7 +402,7 @@ jobs:
|
|
|
364
402
|
${{ env.INCOMPLETE_COMMENT }}
|
|
365
403
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
366
404
|
|
|
367
|
-
if: inputs.issue-number != ''
|
|
405
|
+
if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true'
|
|
368
406
|
|
|
369
407
|
runs-on: agents-arc
|
|
370
408
|
runs-on-slim: agents-arc
|
|
@@ -440,7 +478,9 @@ timeout-minutes: 90
|
|
|
440
478
|
|
|
441
479
|
- On a `${{ env.INITIAL_MODE }}` pass, refine from scratch.
|
|
442
480
|
- On a `${{ env.RESPONSE_MODE }}` pass, incorporate only the supplied answers from the issue author or an
|
|
443
|
-
assignee. Do not use answers from other commenters.
|
|
481
|
+
assignee. Do not use answers from other commenters. The body may already hold the
|
|
482
|
+
temporal draft from the earlier pass: reuse what still holds, and resolve its pending
|
|
483
|
+
marks with the author's answers.
|
|
444
484
|
|
|
445
485
|
3. Explore before you write. Call skill("pc-plan-explore"); it owns the stance for this step.
|
|
446
486
|
|
|
@@ -484,8 +524,8 @@ timeout-minutes: 90
|
|
|
484
524
|
No "As a / I want / so that" form. No Given/When/Then. No Mermaid. Just the marker,
|
|
485
525
|
the summary, and the checklist.
|
|
486
526
|
|
|
487
|
-
Load `@humanizer` and prepare the replacement issue body, then go directly to step
|
|
488
|
-
(estimate). Skip steps 5
|
|
527
|
+
Load `@humanizer` and prepare the replacement issue body, then go directly to step 8
|
|
528
|
+
(estimate). Skip steps 5-7.
|
|
489
529
|
|
|
490
530
|
5. Before writing the story, verify coverage: list every work unit and confirm each one has
|
|
491
531
|
exploration findings concrete enough for acceptance criteria. If any unit is missing, go back
|
|
@@ -499,9 +539,36 @@ timeout-minutes: 90
|
|
|
499
539
|
Apply repository documentation and established conventions before finalizing the story.
|
|
500
540
|
Adhere to ${{ env.REPO_RULES }}.
|
|
501
541
|
|
|
502
|
-
6.
|
|
542
|
+
6. **Wrap the story in the repository's issue form.** The body people read must follow the
|
|
543
|
+
repository's own issue template when one exists; the story is the content, the template
|
|
544
|
+
is the shape.
|
|
545
|
+
|
|
546
|
+
Find the form first:
|
|
547
|
+
|
|
548
|
+
- List the YAML and Markdown forms under `.github/ISSUE_TEMPLATE/`, plus a legacy
|
|
549
|
+
`.github/issue_template.md` or a root `template.yml`. `config.yml` there only declares
|
|
550
|
+
contact links, which are not forms: ignore it.
|
|
551
|
+
- When a form filters by labels and the issue carries one of those labels, that form
|
|
552
|
+
wins. Otherwise use the repository's default form.
|
|
553
|
+
- When the repository has no form at all, keep the free-form story shape from step 5:
|
|
554
|
+
there is nothing to wrap around.
|
|
555
|
+
|
|
556
|
+
Then fill it:
|
|
503
557
|
|
|
504
|
-
|
|
558
|
+
- Draw every field's content from your exploration findings. Required fields always get
|
|
559
|
+
real content; optional fields only when you genuinely have something for them.
|
|
560
|
+
- The story narrative lands in the field that asks for it — proposal, description, or
|
|
561
|
+
what-happened, depending on the form.
|
|
562
|
+
- The Given/When/Then scenarios go into the form's acceptance-criteria field when it has
|
|
563
|
+
one; otherwise they stay a section of their own. The Mermaid diagram goes where it
|
|
564
|
+
reads best inside the filled form.
|
|
565
|
+
- The machine-readable lines the later steps add — split markers in step 9, estimate
|
|
566
|
+
lines in step 10 — always sit at the very top of the body, above the form's first
|
|
567
|
+
heading, so the workflow can read them whatever the form's shape.
|
|
568
|
+
|
|
569
|
+
7. Load `@humanizer` and prepare the complete replacement issue body as valid Markdown.
|
|
570
|
+
|
|
571
|
+
8. **Estimate the story in points.** Use the Fibonacci scale, where one point is roughly one
|
|
505
572
|
human day of work for a developer who knows this codebase. Estimate the whole story: code,
|
|
506
573
|
tests, and the edge cases the acceptance criteria imply.
|
|
507
574
|
|
|
@@ -514,7 +581,7 @@ timeout-minutes: 90
|
|
|
514
581
|
Elapsed clock time is not evidence. A large change can land in minutes and a small one can
|
|
515
582
|
wait days for a human, so never reason from how long anything took.
|
|
516
583
|
|
|
517
|
-
|
|
584
|
+
9. **Split when the estimate is ${{ env.SPLIT_THRESHOLD }} or more.** An oversized story is the
|
|
518
585
|
single best predictor of a pull request that never lands.
|
|
519
586
|
|
|
520
587
|
First test whether it *can* split. A story splits when it contains slices that are each
|
|
@@ -523,7 +590,7 @@ timeout-minutes: 90
|
|
|
523
590
|
on its own cannot be verified.
|
|
524
591
|
|
|
525
592
|
**If it splits:** write between two and ${{ env.MAX_SPLIT_CHILDREN }} children. Each child is
|
|
526
|
-
a complete refined story in the same
|
|
593
|
+
a complete refined story wrapped in the same issue form, with its own
|
|
527
594
|
acceptance criteria, its own tests section, and its own estimate of 5 or less. Never write a
|
|
528
595
|
child estimated at 1: that is a fragment, so fold it into a sibling. Call `create_issue` once
|
|
529
596
|
per child, and in each child body include:
|
|
@@ -539,7 +606,7 @@ timeout-minutes: 90
|
|
|
539
606
|
single story and say so in one sentence in the body, under the estimate. An honest 8 is more
|
|
540
607
|
useful than three fake threes that each break the build.
|
|
541
608
|
|
|
542
|
-
|
|
609
|
+
10. **Record the estimate in every body you write**, parent and children alike, immediately below
|
|
543
610
|
the title line, as exactly these two lines:
|
|
544
611
|
|
|
545
612
|
```
|
|
@@ -550,12 +617,22 @@ timeout-minutes: 90
|
|
|
550
617
|
The visible line is for people and the marker is read by the workflow, which turns it into the
|
|
551
618
|
`sp-N` label. A body without the marker gets no estimate label at all.
|
|
552
619
|
|
|
553
|
-
|
|
620
|
+
11. Decide exactly one outcome:
|
|
554
621
|
|
|
555
622
|
Labels are workflow-owned state. Do not call `add_labels` or `remove_labels`.
|
|
556
623
|
|
|
557
624
|
**Questions remain.** You set aside one or more questions for the author that the codebase
|
|
558
|
-
could not answer. Leave the
|
|
625
|
+
could not answer. Leave the partial work visible: first call `update_issue` with a
|
|
626
|
+
temporal draft, then call `add_comment` once with the questions.
|
|
627
|
+
|
|
628
|
+
The temporal draft is the replacement body your path would have written — the filled
|
|
629
|
+
issue form from step 6 on the standard path, the marker, summary and checklist from
|
|
630
|
+
step 4a on the trivial path — holding everything you already established, with every
|
|
631
|
+
part the questions leave open marked `_pending — see questions below_`. Its very
|
|
632
|
+
first line is `${{ env.DRAFT_MARKER }}`; the next run replaces the draft wholesale
|
|
633
|
+
with the final body.
|
|
634
|
+
|
|
635
|
+
The comment carries:
|
|
559
636
|
1. `${{ env.REFINE_MARKER }}`
|
|
560
637
|
2. `${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`
|
|
561
638
|
3. `I have some questions about this issue. Please reply in one comment and I'll process your answers.`
|
|
@@ -565,10 +642,9 @@ timeout-minutes: 90
|
|
|
565
642
|
them is a domain expert, not an engineer.
|
|
566
643
|
|
|
567
644
|
**The story is complete.** You answered every exploration question yourself and none remain
|
|
568
|
-
for the author.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
the whole body in that one call: it is the only thing this step has to get right.
|
|
645
|
+
for the author. Call `update_issue` first, with the wrapped replacement body, and wait
|
|
646
|
+
for it to come back. Send the whole body in that one call: it is the only thing this
|
|
647
|
+
step has to get right.
|
|
572
648
|
|
|
573
649
|
Only once that call has succeeded, call `add_comment`
|
|
574
650
|
with `${{ env.REFINE_MARKER }}`, then `${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`,
|
|
@@ -586,5 +662,5 @@ timeout-minutes: 90
|
|
|
586
662
|
seams. Call `create_issue` once per child, then `update_issue` on the parent with the
|
|
587
663
|
summary and the checklist, then `add_comment` with `${{ env.REFINE_MARKER }}`, then
|
|
588
664
|
`${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`, then one sentence naming the estimate you gave the
|
|
589
|
-
|
|
590
|
-
|
|
665
|
+
whole and how many children you wrote. The children carry the work forward; the parent stays
|
|
666
|
+
open as their tracker and is never implemented directly.
|
|
@@ -29,8 +29,8 @@ on:
|
|
|
29
29
|
type: string
|
|
30
30
|
default: "auto"
|
|
31
31
|
|
|
32
|
-
runs-on:
|
|
33
|
-
runs-on-slim:
|
|
32
|
+
runs-on: RunnerLandingZone
|
|
33
|
+
runs-on-slim: RunnerLandingZone
|
|
34
34
|
|
|
35
35
|
secrets:
|
|
36
36
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
@@ -98,7 +98,7 @@ jobs:
|
|
|
98
98
|
always() &&
|
|
99
99
|
needs.agent.result == 'success' &&
|
|
100
100
|
needs.safe_outputs.result == 'success'
|
|
101
|
-
runs-on:
|
|
101
|
+
runs-on: RunnerLandingZone
|
|
102
102
|
permissions:
|
|
103
103
|
contents: write
|
|
104
104
|
steps:
|
|
@@ -205,7 +205,7 @@ jobs:
|
|
|
205
205
|
if: >
|
|
206
206
|
always() &&
|
|
207
207
|
(needs.agent.result != 'success' || needs.safe_outputs.result != 'success')
|
|
208
|
-
runs-on:
|
|
208
|
+
runs-on: RunnerLandingZone
|
|
209
209
|
permissions:
|
|
210
210
|
contents: read
|
|
211
211
|
steps:
|
|
@@ -70,9 +70,41 @@ on:
|
|
|
70
70
|
required: false
|
|
71
71
|
type: string
|
|
72
72
|
default: first
|
|
73
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
74
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
75
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
76
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
77
|
+
# uses for protected_changes.
|
|
78
|
+
needs: [still_open]
|
|
73
79
|
|
|
74
80
|
jobs:
|
|
81
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
82
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
83
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
84
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
85
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
86
|
+
# and gate both the reservation and the agent on it.
|
|
87
|
+
still_open:
|
|
88
|
+
runs-on: agents-arc
|
|
89
|
+
permissions:
|
|
90
|
+
contents: read
|
|
91
|
+
issues: read
|
|
92
|
+
outputs:
|
|
93
|
+
open: ${{ steps.state.outputs.open }}
|
|
94
|
+
steps:
|
|
95
|
+
- name: Checkout workflow actions
|
|
96
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
97
|
+
with:
|
|
98
|
+
persist-credentials: false
|
|
99
|
+
- name: Read the issue state
|
|
100
|
+
id: state
|
|
101
|
+
uses: ./.github/actions/require-open-issue
|
|
102
|
+
with:
|
|
103
|
+
token: ${{ github.token }}
|
|
104
|
+
issue-number: ${{ inputs.issue-number }}
|
|
75
105
|
reserve:
|
|
106
|
+
needs: [still_open]
|
|
107
|
+
if: needs.still_open.outputs.open == 'true'
|
|
76
108
|
runs-on: agents-arc
|
|
77
109
|
permissions:
|
|
78
110
|
contents: read
|
|
@@ -296,7 +328,7 @@ jobs:
|
|
|
296
328
|
${{ env.INCOMPLETE_COMMENT }}
|
|
297
329
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
298
330
|
|
|
299
|
-
if: inputs.issue-number != ''
|
|
331
|
+
if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true'
|
|
300
332
|
|
|
301
333
|
runs-on: agents-arc
|
|
302
334
|
runs-on-slim: agents-arc
|
|
@@ -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: ubuntu-latest
|
|
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: ubuntu-latest
|
|
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"
|