@plainconceptsplatform/workflows 1.3.1 → 1.4.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/worker-env.js +2 -3
- 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 +5 -0
- 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 +64 -3
- 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 +95 -1
- 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-refine-output/validate-refine-output.sh +30 -1
- 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 +17 -0
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +214 -7
- package/loops/docs/diagrams.md +90 -12
- package/loops/scripts/compile-agent-workflows.mjs +76 -2
- 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-implement.md +37 -7
- package/loops/workflows/agent-merge-gate.md +74 -6
- package/loops/workflows/agent-refine.md +152 -5
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/platform-defaults.md +23 -23
- package/loops/workflows/work-router.yml +330 -2
- package/package.json +1 -1
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
name: Feature request
|
|
2
|
-
description: Propose a small improvement or new capability
|
|
3
|
-
labels: ["enhancement"]
|
|
4
|
-
body:
|
|
5
|
-
- type: markdown
|
|
6
|
-
attributes:
|
|
7
|
-
value: |
|
|
8
|
-
This template is for small, well-scoped improvements a focused
|
|
9
|
-
enhancement, a missing convenience, a quality-of-life fix.
|
|
10
|
-
|
|
11
|
-
For large features, architecture changes, or cross-cutting work,
|
|
12
|
-
open a planning issue or an ADR instead.
|
|
13
|
-
|
|
14
|
-
Do **not** include real secrets or production data use placeholders.
|
|
15
|
-
- type: dropdown
|
|
16
|
-
id: scope
|
|
17
|
-
attributes:
|
|
18
|
-
label: Scope
|
|
19
|
-
description: How big is this change? If it doesn't fit Small or Medium, use a planning issue instead.
|
|
20
|
-
options:
|
|
21
|
-
- Small a single file or component, no refactoring
|
|
22
|
-
- Medium touches a few files or a feature area, may need minor refactoring
|
|
23
|
-
validations:
|
|
24
|
-
required: true
|
|
25
|
-
- type: textarea
|
|
26
|
-
id: current-behavior
|
|
27
|
-
attributes:
|
|
28
|
-
label: Current behavior
|
|
29
|
-
description: How does it work today? Describe the existing behavior or limitation.
|
|
30
|
-
validations:
|
|
31
|
-
required: true
|
|
32
|
-
- type: textarea
|
|
33
|
-
id: problem
|
|
34
|
-
attributes:
|
|
35
|
-
label: Problem / motivation
|
|
36
|
-
description: What user or business need does this address? Why is the current behavior a problem?
|
|
37
|
-
validations:
|
|
38
|
-
required: true
|
|
39
|
-
- type: textarea
|
|
40
|
-
id: proposal
|
|
41
|
-
attributes:
|
|
42
|
-
label: Proposed solution
|
|
43
|
-
description: What would you like to happen?
|
|
44
|
-
validations:
|
|
45
|
-
required: true
|
|
46
|
-
- type: textarea
|
|
47
|
-
id: alternatives
|
|
48
|
-
attributes:
|
|
49
|
-
label: Alternatives considered
|
|
50
|
-
validations:
|
|
51
|
-
required: false
|
|
52
|
-
- type: textarea
|
|
53
|
-
id: acceptance
|
|
54
|
-
attributes:
|
|
55
|
-
label: Acceptance criteria
|
|
56
|
-
description: A checklist a reviewer can verify when this is done (include the tests).
|
|
57
|
-
placeholder: |
|
|
58
|
-
- [ ] ...
|
|
59
|
-
- [ ] Tests
|
|
60
|
-
validations:
|
|
61
|
-
required: false
|
|
62
|
-
- type: textarea
|
|
63
|
-
id: open-questions
|
|
64
|
-
attributes:
|
|
65
|
-
label: Open questions
|
|
66
|
-
description: Decisions a maintainer must make before this can be planned. Leave blank if none.
|
|
67
|
-
validations:
|
|
68
|
-
required: false
|
|
69
|
-
- type: textarea
|
|
70
|
-
id: related
|
|
71
|
-
attributes:
|
|
72
|
-
label: Related links
|
|
73
|
-
description: Links to related issues, PRs, discussions, or docs. Leave blank if none.
|
|
74
|
-
validations:
|
|
75
|
-
required: false
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Propose a small improvement or new capability
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
This template is for small, well-scoped improvements a focused
|
|
9
|
+
enhancement, a missing convenience, a quality-of-life fix.
|
|
10
|
+
|
|
11
|
+
For large features, architecture changes, or cross-cutting work,
|
|
12
|
+
open a planning issue or an ADR instead.
|
|
13
|
+
|
|
14
|
+
Do **not** include real secrets or production data use placeholders.
|
|
15
|
+
- type: dropdown
|
|
16
|
+
id: scope
|
|
17
|
+
attributes:
|
|
18
|
+
label: Scope
|
|
19
|
+
description: How big is this change? If it doesn't fit Small or Medium, use a planning issue instead.
|
|
20
|
+
options:
|
|
21
|
+
- Small a single file or component, no refactoring
|
|
22
|
+
- Medium touches a few files or a feature area, may need minor refactoring
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: current-behavior
|
|
27
|
+
attributes:
|
|
28
|
+
label: Current behavior
|
|
29
|
+
description: How does it work today? Describe the existing behavior or limitation.
|
|
30
|
+
validations:
|
|
31
|
+
required: true
|
|
32
|
+
- type: textarea
|
|
33
|
+
id: problem
|
|
34
|
+
attributes:
|
|
35
|
+
label: Problem / motivation
|
|
36
|
+
description: What user or business need does this address? Why is the current behavior a problem?
|
|
37
|
+
validations:
|
|
38
|
+
required: true
|
|
39
|
+
- type: textarea
|
|
40
|
+
id: proposal
|
|
41
|
+
attributes:
|
|
42
|
+
label: Proposed solution
|
|
43
|
+
description: What would you like to happen?
|
|
44
|
+
validations:
|
|
45
|
+
required: true
|
|
46
|
+
- type: textarea
|
|
47
|
+
id: alternatives
|
|
48
|
+
attributes:
|
|
49
|
+
label: Alternatives considered
|
|
50
|
+
validations:
|
|
51
|
+
required: false
|
|
52
|
+
- type: textarea
|
|
53
|
+
id: acceptance
|
|
54
|
+
attributes:
|
|
55
|
+
label: Acceptance criteria
|
|
56
|
+
description: A checklist a reviewer can verify when this is done (include the tests).
|
|
57
|
+
placeholder: |
|
|
58
|
+
- [ ] ...
|
|
59
|
+
- [ ] Tests
|
|
60
|
+
validations:
|
|
61
|
+
required: false
|
|
62
|
+
- type: textarea
|
|
63
|
+
id: open-questions
|
|
64
|
+
attributes:
|
|
65
|
+
label: Open questions
|
|
66
|
+
description: Decisions a maintainer must make before this can be planned. Leave blank if none.
|
|
67
|
+
validations:
|
|
68
|
+
required: false
|
|
69
|
+
- type: textarea
|
|
70
|
+
id: related
|
|
71
|
+
attributes:
|
|
72
|
+
label: Related links
|
|
73
|
+
description: Links to related issues, PRs, discussions, or docs. Leave blank if none.
|
|
74
|
+
validations:
|
|
75
|
+
required: false
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/release/github-release.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: Publish GitHub release
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
push:
|
|
6
|
-
tags:
|
|
7
|
-
- "v*"
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
publish:
|
|
14
|
-
name: Publish release
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
timeout-minutes: 10
|
|
17
|
-
steps:
|
|
18
|
-
- name: Create or update GitHub release
|
|
19
|
-
env:
|
|
20
|
-
GH_TOKEN: ${{ github.token }}
|
|
21
|
-
TAG: ${{ github.ref_name }}
|
|
22
|
-
shell: bash
|
|
23
|
-
run: |
|
|
24
|
-
set -euo pipefail
|
|
25
|
-
|
|
26
|
-
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
27
|
-
gh release edit "$TAG" --title "$TAG" --generate-notes
|
|
28
|
-
else
|
|
29
|
-
gh release create "$TAG" --title "$TAG" --generate-notes
|
|
30
|
-
fi
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/release/github-release.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Publish GitHub release
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Publish release
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
steps:
|
|
18
|
+
- name: Create or update GitHub release
|
|
19
|
+
env:
|
|
20
|
+
GH_TOKEN: ${{ github.token }}
|
|
21
|
+
TAG: ${{ github.ref_name }}
|
|
22
|
+
shell: bash
|
|
23
|
+
run: |
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
27
|
+
gh release edit "$TAG" --title "$TAG" --generate-notes
|
|
28
|
+
else
|
|
29
|
+
gh release create "$TAG" --title "$TAG" --generate-notes
|
|
30
|
+
fi
|
|
@@ -77,8 +77,8 @@ description: |
|
|
|
77
77
|
Waiting on CI inside this run would hold a runner doing nothing, which is why the gate is
|
|
78
78
|
a separate workflow rather than a later step.
|
|
79
79
|
|
|
80
|
-
Router-
|
|
81
|
-
Contract input: issue-number.
|
|
80
|
+
Router-first worker: triggered via workflow_call from work-router.yml, or via workflow_dispatch
|
|
81
|
+
from the router's stack path (`--ref` = the feature branch). Contract input: issue-number.
|
|
82
82
|
|
|
83
83
|
name: "Agent: Implement Issue"
|
|
84
84
|
|
|
@@ -101,6 +101,23 @@ on:
|
|
|
101
101
|
required: false
|
|
102
102
|
type: string
|
|
103
103
|
default: '0'
|
|
104
|
+
# Stack dispatch (work-router.yml reads `<!-- stack-root-branch: feat/N -->` on the issue and
|
|
105
|
+
# runs `gh workflow run agent-implement.lock.yml --ref feat/N`). Under workflow_call
|
|
106
|
+
# GITHUB_SHA is the router event's SHA -- global default-branch state, not the feature
|
|
107
|
+
# branch -- which widens the patch window; dispatch pins the SHA to the feature tip and makes
|
|
108
|
+
# the generator produce exactly-the-child's-commits patches. Workflow-level inputs resolve
|
|
109
|
+
# identically from both triggers, so every job below reads inputs.issue-number unchanged.
|
|
110
|
+
workflow_dispatch:
|
|
111
|
+
inputs:
|
|
112
|
+
issue-number:
|
|
113
|
+
description: Issue number to implement.
|
|
114
|
+
required: true
|
|
115
|
+
type: string
|
|
116
|
+
attempts_so_far:
|
|
117
|
+
description: Failed implement runs already made for this issue. Parked when it reaches the cap.
|
|
118
|
+
required: false
|
|
119
|
+
type: string
|
|
120
|
+
default: '0'
|
|
104
121
|
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
105
122
|
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
106
123
|
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
@@ -619,6 +636,17 @@ safe-outputs:
|
|
|
619
636
|
protected-files: allowed
|
|
620
637
|
allowed-files:
|
|
621
638
|
- "**"
|
|
639
|
+
# The pull request targets the branch this run was started on, which is the branch the work
|
|
640
|
+
# was cut from: `main` when the router calls this worker, `feat/N` when it dispatches a stack
|
|
641
|
+
# child with `--ref feat/N`. Taken from the run rather than asked of the agent, because the
|
|
642
|
+
# agent was asked and did not: on Lyceum #72 it emitted branch and title with no base, the
|
|
643
|
+
# handler fell back to the default branch, and a bundle built on feat/44 could not be replayed
|
|
644
|
+
# onto main (run 38694942046). A repository with no stack markers is never dispatched, so its
|
|
645
|
+
# base stays `main` exactly as before.
|
|
646
|
+
base-branch: ${{ github.ref_name }}
|
|
647
|
+
allowed-base-branches:
|
|
648
|
+
- "main"
|
|
649
|
+
- "feat/*"
|
|
622
650
|
|
|
623
651
|
# The fleet is two machines, so this clock is also how long a stuck run can hold half of it.
|
|
624
652
|
# 240 went on to every worker at once when the provider was slow, which fixed the deaths and
|
|
@@ -728,12 +756,14 @@ timeout-minutes: 180
|
|
|
728
756
|
|
|
729
757
|
Choose exactly one:
|
|
730
758
|
|
|
731
|
-
- **`safeoutputs/create_pull_request`** , the normal path. Propose a pull request
|
|
732
|
-
|
|
759
|
+
- **`safeoutputs/create_pull_request`** , the normal path. Propose a pull request with the
|
|
760
|
+
verified changes. Do not pass `base`: the workflow sets it to the branch this run started
|
|
761
|
+
on, which is the branch you are working from, so a stack child lands on its feature branch
|
|
762
|
+
without you doing anything. The `body` **must close exactly this issue and nothing else**
|
|
733
763
|
(`Closes #${{ inputs.issue-number }}`) and summarise what changed and why. Its `branch` is
|
|
734
|
-
the branch you are on, the one named in step 1; do not invent another name. You do not
|
|
735
|
-
to check whether a pull request already exists for this issue: the router does
|
|
736
|
-
dispatching you and does not start this workflow when one does.
|
|
764
|
+
the branch you are on, the one named in step 1; do not invent another name. You do not
|
|
765
|
+
need to check whether a pull request already exists for this issue: the router does
|
|
766
|
+
that before dispatching you and does not start this workflow when one does.
|
|
737
767
|
- **`safeoutputs/report_incomplete`** , only when infrastructure or tooling prevents you
|
|
738
768
|
from completing the task, such as a pre-existing build failure you cannot fix. Provide a
|
|
739
769
|
specific `reason`.
|
|
@@ -89,7 +89,9 @@ description: |
|
|
|
89
89
|
risk assessment is clean, hand to a human when it is not, fix CI when it failed. Called by
|
|
90
90
|
the Work Router; does not trigger on public events.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
In a stack, a child pull request (its base is the feature branch) merges into that feature
|
|
93
|
+
branch and its child issue closes with it; the root pull request is held for a person. The
|
|
94
|
+
router supplies the CI conclusion and run ID as facts, so there is no polling and no
|
|
93
95
|
timeout branch. The conclusion is read from the inputs instead of filtered at the trigger.
|
|
94
96
|
|
|
95
97
|
name: "Agent: Merge Gate"
|
|
@@ -155,6 +157,8 @@ jobs:
|
|
|
155
157
|
conclusion: ${{ steps.subject.outputs.conclusion }}
|
|
156
158
|
run-id: ${{ steps.subject.outputs.run-id }}
|
|
157
159
|
review_blocked: ${{ steps.review.outputs.review_blocked }}
|
|
160
|
+
pr_base: ${{ steps.stack.outputs.pr_base }}
|
|
161
|
+
stack_root: ${{ steps.stack.outputs.stack_root }}
|
|
158
162
|
steps:
|
|
159
163
|
- name: Checkout workflow actions
|
|
160
164
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -189,6 +193,37 @@ jobs:
|
|
|
189
193
|
sleep 5
|
|
190
194
|
done
|
|
191
195
|
echo "review_blocked=$([ "$decision" = 'CHANGES_REQUESTED' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
196
|
+
# A child PR in a stack has its base on the feature branch, not main. The rest of the
|
|
197
|
+
# gate needs both facts measured here, once: which branch this PR merges into, and
|
|
198
|
+
# whether this PR is the root of a stack (its linked feature issue carries this PR's
|
|
199
|
+
# number in the `<!-- stack-root-pr: N -->` marker refine stamped when it created the
|
|
200
|
+
# root PR). The base is always known; the marker only exists in stack repos, so
|
|
201
|
+
# repos without stacks see no change at all in either output ('' and 'false').
|
|
202
|
+
- name: Detect the pull request's base and its role in a stack
|
|
203
|
+
id: stack
|
|
204
|
+
# Only for a resolved subject. Without one, PR is empty and `gh pr view ""` falls back to
|
|
205
|
+
# the checked-out branch and fails, so every gate dispatched for a pull request that is
|
|
206
|
+
# not a subject -- a held stack root whose CI went green, for one -- ended red (Lyceum
|
|
207
|
+
# #106, run 38750643587: "no pull requests found for branch main"). Skipped, both outputs
|
|
208
|
+
# are empty, which everything downstream already reads as "not a root".
|
|
209
|
+
if: steps.subject.outputs.found == 'true'
|
|
210
|
+
env:
|
|
211
|
+
GH_TOKEN: ${{ github.token }}
|
|
212
|
+
REPO: ${{ github.repository }}
|
|
213
|
+
PR: ${{ steps.subject.outputs.pr }}
|
|
214
|
+
ISSUE: ${{ steps.subject.outputs.issue }}
|
|
215
|
+
run: |
|
|
216
|
+
set -euo pipefail
|
|
217
|
+
pr_base=$(gh pr view "$PR" --repo "$REPO" --json baseRefName --jq '.baseRefName // ""')
|
|
218
|
+
stack_root=false
|
|
219
|
+
if [ -n "$ISSUE" ]; then
|
|
220
|
+
body=$(gh issue view "$ISSUE" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null || echo "")
|
|
221
|
+
if echo "$body" | grep -q "<!-- stack-root-pr: $PR -->"; then
|
|
222
|
+
stack_root=true
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
echo "pr_base=$pr_base" >> "$GITHUB_OUTPUT"
|
|
226
|
+
echo "stack_root=$stack_root" >> "$GITHUB_OUTPUT"
|
|
192
227
|
|
|
193
228
|
# Rung 4b. Everything the merge decision needs that a shell can establish, measured once,
|
|
194
229
|
# before any model reads the diff. The job kept its name and its `requires_review` and
|
|
@@ -525,7 +560,7 @@ jobs:
|
|
|
525
560
|
Findings and verification on the linked issue: #${{ needs.subject.outputs.issue }}. [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
526
561
|
${{ env.EVIDENCE_BEFORE_REVIEW == 'true' && contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Screenshots of this head follow in their own comment if it carries a verification plan. Approving the pull request merges it.' || '' }}
|
|
527
562
|
- name: Merge approved pull request
|
|
528
|
-
if: needs.validate_output.outputs.outcome == 'auto-merge'
|
|
563
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root != 'true'
|
|
529
564
|
env:
|
|
530
565
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
531
566
|
REPO: ${{ github.repository }}
|
|
@@ -534,6 +569,20 @@ jobs:
|
|
|
534
569
|
set -euo pipefail
|
|
535
570
|
head_sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
536
571
|
gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
|
|
572
|
+
# The root's own gate run stops here: children may still be landing on the feature
|
|
573
|
+
# branch, and the merge decision for main belongs to a person. The comment says only
|
|
574
|
+
# what a reader needs: the gate held the root, children merge into the feature branch,
|
|
575
|
+
# and a human merges the root when the stack completes.
|
|
576
|
+
- name: Hold a stack root for a human merge
|
|
577
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true'
|
|
578
|
+
uses: ./.github/actions/create-issue-comment
|
|
579
|
+
with:
|
|
580
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
581
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
582
|
+
body: |
|
|
583
|
+
**Stack root held.** This pull request is the root of a stack: its children merge
|
|
584
|
+
into the feature branch, and it merges into ${{ github.event.repository.default_branch }} only when a person
|
|
585
|
+
does it, after all children have landed and the feature branch validates.
|
|
537
586
|
- name: Release remediated issue
|
|
538
587
|
if: needs.validate_output.outputs.outcome == 'remediated'
|
|
539
588
|
uses: ./.github/actions/remove-issue-labels
|
|
@@ -583,6 +632,22 @@ jobs:
|
|
|
583
632
|
token: ${{ steps.app-token.outputs.token }}
|
|
584
633
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
585
634
|
labels: ${{ env.WORKING_LABEL }}
|
|
635
|
+
# GitHub's closing keywords never fire when a PR merges into a branch other than the
|
|
636
|
+
# default, so a child PR landing on the feature branch leaves its issue open — and
|
|
637
|
+
# "zero open children" would never be reachable. This step closes the child issue
|
|
638
|
+
# deterministically, exactly the issue this PR implements, with the App token: the
|
|
639
|
+
# close raises routed events only for issue lifecycle the router already understands,
|
|
640
|
+
# and `gh issue close` on an already-closed issue is a harmless no-op, making the
|
|
641
|
+
# step safe to re-run.
|
|
642
|
+
- name: Close the child issue when the pull request landed on a feature branch
|
|
643
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.pr_base != '' && needs.subject.outputs.pr_base != github.event.repository.default_branch
|
|
644
|
+
env:
|
|
645
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
646
|
+
REPO: ${{ github.repository }}
|
|
647
|
+
ISSUE: ${{ needs.subject.outputs.issue }}
|
|
648
|
+
run: |
|
|
649
|
+
set -euo pipefail
|
|
650
|
+
gh issue close "$ISSUE" --repo "$REPO" --reason completed || true
|
|
586
651
|
# A person decides this one, so the screenshots come before the decision, in a router run of
|
|
587
652
|
# their own outside the belt. Never on auto-merge: that dispatch would race the merge.
|
|
588
653
|
- name: Replay the verification plan for the reviewer
|
|
@@ -934,8 +999,10 @@ timeout-minutes: 120
|
|
|
934
999
|
4b. **Merge conflict when CI is green.** If the conclusion is `success` and
|
|
935
1000
|
`has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
|
|
936
1001
|
resolve the conflict before assessing risk. You are already on the PR branch.
|
|
937
|
-
Merge `origin/${{
|
|
938
|
-
(`git merge origin/${{
|
|
1002
|
+
Merge `origin/${{ needs.subject.outputs.pr_base }}` into it
|
|
1003
|
+
(`git merge origin/${{ needs.subject.outputs.pr_base }}`; that is the branch this pull
|
|
1004
|
+
request merges into — the default branch for a flat pull request, the feature branch
|
|
1005
|
+
for a stack child), resolve every
|
|
939
1006
|
conflict deliberately, commit the merge, and run the verification commands below. Do not
|
|
940
1007
|
use `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the
|
|
941
1008
|
intended behavior from both sides, and never rebase: the push is fast-forward only.
|
|
@@ -1098,8 +1165,9 @@ timeout-minutes: 120
|
|
|
1098
1165
|
must not auto-merge the PR.
|
|
1099
1166
|
|
|
1100
1167
|
**If `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`):** You are already on the PR branch. Resolve the conflict;
|
|
1101
|
-
it is not a reason to hand the PR to a human. Merge `origin/${{
|
|
1102
|
-
into the current branch, resolve every conflict
|
|
1168
|
+
it is not a reason to hand the PR to a human. Merge `origin/${{ needs.subject.outputs.pr_base }}`
|
|
1169
|
+
(the branch this pull request merges into) into the current branch, resolve every conflict
|
|
1170
|
+
deliberately, stage the resolutions, and
|
|
1103
1171
|
commit the merge. Then run verification and push the resulting branch update. Do not use
|
|
1104
1172
|
`--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
|
|
1105
1173
|
behavior from both sides.
|
|
@@ -53,6 +53,7 @@ env:
|
|
|
53
53
|
SPLIT_PARENT_PREFIX: "<!-- split-parent: "
|
|
54
54
|
SPLIT_THRESHOLD: "8"
|
|
55
55
|
MAX_SPLIT_CHILDREN: "6"
|
|
56
|
+
MIN_CHILD_POINTS: "3"
|
|
56
57
|
INCOMPLETE_COMMENT: "Automated refinement ended without an outcome. The refine label remains for a retry."
|
|
57
58
|
SAFE_OUTPUT_COMMENT_PREFIX: "Refinement update"
|
|
58
59
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
|
|
@@ -396,6 +397,7 @@ jobs:
|
|
|
396
397
|
update-issues: 'true'
|
|
397
398
|
create-issues: 'true'
|
|
398
399
|
fallback-issue-number: ${{ inputs.issue-number }}
|
|
400
|
+
parent-issue: ${{ needs.validate_output.outputs.outcome == 'split' && inputs.issue-number || '' }}
|
|
399
401
|
- name: Apply complete refinement labels
|
|
400
402
|
if: needs.validate_output.outputs.outcome == 'complete'
|
|
401
403
|
uses: ./.github/actions/add-issue-labels
|
|
@@ -434,6 +436,83 @@ jobs:
|
|
|
434
436
|
${{ env.REFINE_LABEL }}
|
|
435
437
|
${{ env.WORKING_LABEL }}
|
|
436
438
|
${{ env.REVIEW_LABEL }}
|
|
439
|
+
# The stack root pull request. Every output in this worker is staged and applied by this
|
|
440
|
+
# job, and gh-aw's handler opens nothing while the run is staged: a per-output
|
|
441
|
+
# `staged: false` on create-pull-request compiles into the handler config and is ignored
|
|
442
|
+
# there (Lyceum #45, run 38740216266: "Pull request creation preview written to step
|
|
443
|
+
# summary"). So the root is opened here, from the agent's own create_pull_request request,
|
|
444
|
+
# once the validator has called the run a split. Refine writes no code, so the root carries
|
|
445
|
+
# none: an empty commit on the default branch's tip, created through the API, which is what
|
|
446
|
+
# gh-aw's allow-empty would have pushed. Idempotent: an open root on the feature branch is
|
|
447
|
+
# reused and an existing branch is not recreated, so a retry stamps the same root.
|
|
448
|
+
- name: Open the stack root pull request
|
|
449
|
+
id: root
|
|
450
|
+
if: needs.validate_output.outputs.outcome == 'split'
|
|
451
|
+
continue-on-error: true
|
|
452
|
+
env:
|
|
453
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
454
|
+
REPO: ${{ github.repository }}
|
|
455
|
+
ISSUE: ${{ inputs.issue-number }}
|
|
456
|
+
AGENT_OUTPUT: agent-artifact/agent_output.json
|
|
457
|
+
run: |
|
|
458
|
+
set -euo pipefail
|
|
459
|
+
branch="feat/${ISSUE}"
|
|
460
|
+
item=$(jq -c '[.items[] | select(.type == "create_pull_request")][0] // {}' "$AGENT_OUTPUT")
|
|
461
|
+
title=$(printf '%s' "$item" | jq -r '.title // empty')
|
|
462
|
+
body=$(printf '%s' "$item" | jq -r '.body // empty')
|
|
463
|
+
if [ -z "$title" ]; then
|
|
464
|
+
title=$(gh issue view "$ISSUE" --repo "$REPO" --json title --jq '.title')
|
|
465
|
+
fi
|
|
466
|
+
case "$title" in "[bot] "*) ;; *) title="[bot] ${title}" ;; esac
|
|
467
|
+
# The root's merge is what ends the feature, so it must close the feature issue.
|
|
468
|
+
if ! printf '%s' "$body" | grep -qiE "(fixes|closes|resolves) #${ISSUE}([^0-9]|$)"; then
|
|
469
|
+
body=$(printf '%s\n\nFixes #%s' "$body" "$ISSUE")
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
existing=$(gh pr list --repo "$REPO" --state open --head "$branch" --json number --jq '.[0].number // empty')
|
|
473
|
+
if [ -n "$existing" ]; then
|
|
474
|
+
echo "Reusing open root #${existing} on ${branch}"
|
|
475
|
+
echo "pr_number=${existing}" >> "$GITHUB_OUTPUT"
|
|
476
|
+
exit 0
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
default=$(gh api "repos/${REPO}" --jq '.default_branch')
|
|
480
|
+
if ! gh api "repos/${REPO}/git/ref/heads/${branch}" >/dev/null 2>&1; then
|
|
481
|
+
base_sha=$(gh api "repos/${REPO}/git/ref/heads/${default}" --jq '.object.sha')
|
|
482
|
+
tree=$(gh api "repos/${REPO}/git/commits/${base_sha}" --jq '.tree.sha')
|
|
483
|
+
commit=$(gh api "repos/${REPO}/git/commits" -f message="chore: open the stack root for #${ISSUE}" \
|
|
484
|
+
-f tree="$tree" -f "parents[]=${base_sha}" --jq '.sha')
|
|
485
|
+
gh api "repos/${REPO}/git/refs" -f ref="refs/heads/${branch}" -f sha="$commit" >/dev/null
|
|
486
|
+
fi
|
|
487
|
+
printf '%s' "$body" > /tmp/root-body.md
|
|
488
|
+
url=$(gh pr create --repo "$REPO" --base "$default" --head "$branch" --title "$title" --body-file /tmp/root-body.md)
|
|
489
|
+
echo "Opened root ${url} on ${branch}"
|
|
490
|
+
echo "pr_number=${url##*/}" >> "$GITHUB_OUTPUT"
|
|
491
|
+
# The stack markers. The feature branch and its root pull request are what every child
|
|
492
|
+
# builds on and what completion is counted against, so they are recorded on the feature
|
|
493
|
+
# issue itself, where the child implement runs and the router read them back with no new
|
|
494
|
+
# service: `<!-- stack-root-branch: ... -->` resolves each child's dispatch ref, and
|
|
495
|
+
# `<!-- stack-root-pr: ... -->` names the held root. The branch comes from the pull
|
|
496
|
+
# request itself, never from the name the agent chose, because what reached the remote
|
|
497
|
+
# is the only truth. Idempotent: old markers are replaced, so a re-split after a
|
|
498
|
+
# discarded first attempt re-stamps cleanly. Missing markers fail closed -- a child
|
|
499
|
+
# without them takes the flat path, never a silent feature-branch dispatch.
|
|
500
|
+
- name: Record the stack root on the issue
|
|
501
|
+
if: needs.validate_output.outputs.outcome == 'split' && (steps.root.outputs.pr_number != '' || needs.safe_outputs.outputs.created_pr_number != '')
|
|
502
|
+
continue-on-error: true
|
|
503
|
+
env:
|
|
504
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
505
|
+
REPO: ${{ github.repository }}
|
|
506
|
+
ISSUE: ${{ inputs.issue-number }}
|
|
507
|
+
PR_NUMBER: ${{ steps.root.outputs.pr_number || needs.safe_outputs.outputs.created_pr_number }}
|
|
508
|
+
run: |
|
|
509
|
+
set -euo pipefail
|
|
510
|
+
branch=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefName --jq '.headRefName')
|
|
511
|
+
body=$(gh issue view "$ISSUE" --repo "$REPO" --json body --jq '.body // ""')
|
|
512
|
+
cleaned=$(printf '%s' "$body" | sed -E 's/<!-- stack-root-(branch|pr): [^ ]+ -->//g' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}')
|
|
513
|
+
printf '%s\n\n<!-- stack-root-branch: %s -->\n<!-- stack-root-pr: %s -->' "$cleaned" "$branch" "$PR_NUMBER" > /tmp/issue-body.md
|
|
514
|
+
gh issue edit "$ISSUE" --repo "$REPO" --body-file /tmp/issue-body.md
|
|
515
|
+
echo "Stamped stack root branch $branch and PR #$PR_NUMBER on issue #$ISSUE"
|
|
437
516
|
# Estimates become labels here rather than in the agent, because labels are workflow-owned
|
|
438
517
|
# state. Children were created moments ago by the same run, so they are labelled together
|
|
439
518
|
# with the parent: each body carries its own marker.
|
|
@@ -507,6 +586,7 @@ jobs:
|
|
|
507
586
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
508
587
|
REPO: ${{ github.repository }}
|
|
509
588
|
PARENT: ${{ inputs.issue-number }}
|
|
589
|
+
ROOT_PR: ${{ steps.root.outputs.pr_number || needs.safe_outputs.outputs.created_pr_number }}
|
|
510
590
|
REFINE_LABEL: ${{ env.REFINE_LABEL }}
|
|
511
591
|
REFINED_LABEL: ${{ env.REFINED_LABEL }}
|
|
512
592
|
IMPLEMENT_LABEL: ${{ env.IMPLEMENT_LABEL }}
|
|
@@ -518,6 +598,21 @@ jobs:
|
|
|
518
598
|
children=$(gh issue list --repo "$REPO" --state open --limit 50 \
|
|
519
599
|
--search "\"<!-- split-parent: ${PARENT} -->\" in:body" --json number --jq '.[].number' || true)
|
|
520
600
|
fi
|
|
601
|
+
|
|
602
|
+
# Each child body also carries the resolved feature branch. The implement agent sees
|
|
603
|
+
# only the child issue's body in its context, never the parent's, so a marker that
|
|
604
|
+
# lives on the parent alone is invisible to the very prompt that must act on it.
|
|
605
|
+
# Stamping the branch the root pull request really runs on (headRefName, the same
|
|
606
|
+
# source of truth the parent marker uses) makes the child self-describing end to
|
|
607
|
+
# end: the router reads it to resolve the dispatch ref and the agent reads it to set
|
|
608
|
+
# its pull request base. Idempotent, and skipped entirely when the root pull request
|
|
609
|
+
# was not created -- without a feature branch there is no stack to describe, and the
|
|
610
|
+
# child simply takes the flat path.
|
|
611
|
+
STACK_BRANCH=''
|
|
612
|
+
if [ -n "$ROOT_PR" ]; then
|
|
613
|
+
STACK_BRANCH=$(gh pr view "$ROOT_PR" --repo "$REPO" --json headRefName --jq '.headRefName' 2>/dev/null || true)
|
|
614
|
+
fi
|
|
615
|
+
|
|
521
616
|
for child in $children; do
|
|
522
617
|
[ "$child" = "$PARENT" ] && continue
|
|
523
618
|
# A child handed over earlier carries the refined label. Handing it again would start
|
|
@@ -527,6 +622,12 @@ jobs:
|
|
|
527
622
|
echo "#$child was already handed over; leaving it alone"
|
|
528
623
|
continue
|
|
529
624
|
fi
|
|
625
|
+
if [ -n "$STACK_BRANCH" ]; then
|
|
626
|
+
child_body=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""')
|
|
627
|
+
cleaned=$(printf '%s' "$child_body" | sed -E 's/<!-- stack-root-branch: [^ ]+ -->//g' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}')
|
|
628
|
+
printf '%s\n\n<!-- stack-root-branch: %s -->' "$cleaned" "$STACK_BRANCH" > /tmp/child-body.md
|
|
629
|
+
gh issue edit "$child" --repo "$REPO" --body-file /tmp/child-body.md >/dev/null
|
|
630
|
+
fi
|
|
530
631
|
gh issue edit "$child" --repo "$REPO" \
|
|
531
632
|
--remove-label "$REFINE_LABEL" --remove-label "$IMPLEMENT_LABEL" >/dev/null 2>&1 || true
|
|
532
633
|
gh issue edit "$child" --repo "$REPO" --add-label "$REFINED_LABEL" >/dev/null
|
|
@@ -744,6 +845,23 @@ safe-outputs:
|
|
|
744
845
|
# one issue nobody can land; the cap stops a runaway decomposition.
|
|
745
846
|
create-issue:
|
|
746
847
|
max: 6
|
|
848
|
+
# The stack root. A split story integrates on a feature branch instead of the default
|
|
849
|
+
# branch: the children land there one by one and the root pull request carries the whole
|
|
850
|
+
# feature to the default branch once they are done, which is also the human validation
|
|
851
|
+
# point before main. The branch name is preserved rather than salted because it is a
|
|
852
|
+
# long-lived integration line that children, markers and prompts all address by name;
|
|
853
|
+
# a collision with an existing branch fails the push loudly, which is the correct
|
|
854
|
+
# outcome for a durable ref. The allowed-branches glob must stay in sync with the name
|
|
855
|
+
# the prompt tells the agent to choose (feat/ plus the parent issue number). Only the
|
|
856
|
+
# root targets the default branch, so allowed-base-branches admits nothing else.
|
|
857
|
+
create-pull-request:
|
|
858
|
+
# Staged like everything else, and opened by the `Open the stack root pull request` step:
|
|
859
|
+
# gh-aw ignores a per-output `staged: false` while the worker is staged.
|
|
860
|
+
allowed-branches: ["feat/*"]
|
|
861
|
+
allowed-base-branches: ["main"]
|
|
862
|
+
preserve-branch-name: true
|
|
863
|
+
title-prefix: "[bot] "
|
|
864
|
+
allow-empty: true
|
|
747
865
|
|
|
748
866
|
|
|
749
867
|
# The fleet is two machines, so this clock is also how long a stuck run can hold half of it.
|
|
@@ -820,11 +938,24 @@ timeout-minutes: 90
|
|
|
820
938
|
**If it splits:** write between two and ${{ env.MAX_SPLIT_CHILDREN }} children. Each child is
|
|
821
939
|
a complete refined story wrapped in the same issue form, with its own
|
|
822
940
|
acceptance criteria, its own tests section, and its own estimate of 5 or less. Never write a
|
|
823
|
-
child estimated
|
|
941
|
+
child estimated below ${{ env.MIN_CHILD_POINTS }}: that is a fragment, not a story -- the whole
|
|
942
|
+
stack exists to carry real slices, so fold anything smaller into a sibling and give the
|
|
943
|
+
sibling the honest combined estimate. Call `create_issue` once
|
|
824
944
|
per child, and in each child body include:
|
|
825
945
|
|
|
826
946
|
- the line `${{ env.SPLIT_PARENT_PREFIX }}N -->` naming the parent issue number
|
|
827
|
-
- a `Blocked by #
|
|
947
|
+
- a `Blocked by #aw_<id>` line for each sibling that must land first, when order genuinely
|
|
948
|
+
matters, one line per sibling. Siblings have no numbers yet, so give every child a
|
|
949
|
+
`temporary_id` of `aw_` plus 3 to 12 letters, digits or underscores (`aw_modules`, not
|
|
950
|
+
`aw_module_backend`, which gh-aw rejects as too long) and name siblings by it; the workflow rewrites each
|
|
951
|
+
`#aw_<id>` to the real issue number once the children exist. Write dependencies only as these
|
|
952
|
+
lines and never set the `blocked_by` field: it takes one reference, and a value naming two
|
|
953
|
+
rejected the item and discarded the whole refinement (Lyceum #45).
|
|
954
|
+
- a `Parent feature branch: feat/<parent issue number>` line (for parent #123,
|
|
955
|
+
`Parent feature branch: feat/123`) naming the branch the whole split integrates on:
|
|
956
|
+
each child's pull request targets that branch, never the default branch, the children
|
|
957
|
+
land there one at a time, and only the root pull request carries the finished feature
|
|
958
|
+
to the default branch
|
|
828
959
|
|
|
829
960
|
Then call `update_issue` on the parent, replacing its body with a short summary of the whole
|
|
830
961
|
piece of work, the reason it was split, and a checklist linking every child. The parent keeps
|
|
@@ -914,7 +1045,23 @@ timeout-minutes: 90
|
|
|
914
1045
|
|
|
915
1046
|
**The story was split.** You estimated ${{ env.SPLIT_THRESHOLD }} or more and found real
|
|
916
1047
|
seams. Call `create_issue` once per child, then `update_issue` on the parent with the
|
|
917
|
-
summary and the checklist
|
|
1048
|
+
summary and the checklist. Then open the stack's root pull request, in this order:
|
|
1049
|
+
|
|
1050
|
+
First write the feature's journey as one failing end-to-end test, following the
|
|
1051
|
+
repository's own end-to-end test conventions. That failing test is the specification, not
|
|
1052
|
+
the implementation: write test code only, never the production code that would make it
|
|
1053
|
+
pass. When the repository has no end-to-end test conventions at all, skip the test rather
|
|
1054
|
+
than inventing one -- the root pull request may be proposal-only.
|
|
1055
|
+
|
|
1056
|
+
Then call `create_pull_request` once, with the branch named `feat/` plus the parent issue
|
|
1057
|
+
number -- exactly the name you wrote into every child's `Parent feature branch:` line --
|
|
1058
|
+
a one-line title for the whole feature, and a body carrying that same summary and child
|
|
1059
|
+
checklist plus the closing line `Fixes #${{ inputs.issue-number }}`. Do not pass `base`:
|
|
1060
|
+
the root pull request targets the default branch, and its closing line fires only when it
|
|
1061
|
+
eventually merges there, after every child has landed on the feature branch.
|
|
1062
|
+
|
|
1063
|
+
Finally, call `add_comment` with `${{ env.REFINE_MARKER }}`, then
|
|
918
1064
|
`${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`, then one sentence naming the estimate you gave the
|
|
919
|
-
|
|
920
|
-
|
|
1065
|
+
whole, how many children you wrote, and the feature branch they integrate on. The children
|
|
1066
|
+
carry the work forward; the parent stays open as their tracker and is never implemented
|
|
1067
|
+
directly.
|