@plainconceptsplatform/workflows 1.3.1 → 1.5.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.d.ts +2 -0
- package/dist/catalog-installation.js +44 -2
- package/dist/index.js +3 -0
- package/dist/stack-defaults.d.ts +1 -0
- package/dist/stack-defaults.js +13 -1
- 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-merge-gate-output/action.yml +5 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +8 -2
- 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 +221 -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 +43 -8
- package/loops/workflows/agent-merge-gate.md +102 -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 +345 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-implement.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
|
-
VERIFY_COMMANDS: "
|
|
4
|
+
VERIFY_COMMANDS: ""
|
|
5
5
|
REPO_RULES: "Implement only the selected issue. Follow repository documentation and existing conventions. Do not weaken tests, lower coverage thresholds, or bypass checks."
|
|
6
6
|
# Split out of REPO_RULES because one field asked to carry architecture, testing, coverage and
|
|
7
7
|
# conventions together, and measured on 2026-09-07 three of the four consuming repositories had
|
|
@@ -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
|
|
@@ -713,6 +741,11 @@ timeout-minutes: 180
|
|
|
713
741
|
change and makes you clean the files you touched; fix every line it lists. The merge gate
|
|
714
742
|
runs it again and does not merge while it fails.
|
|
715
743
|
|
|
744
|
+
Anything you change to satisfy `check-diff` is a change: run the verification commands
|
|
745
|
+
again afterwards, and open the pull request only from a tree where both pass. Do not delete
|
|
746
|
+
a test that covers a branch you added to fit the budget; the complexity gate in CI measures
|
|
747
|
+
that coverage.
|
|
748
|
+
|
|
716
749
|
5. Do not touch `changelog.json`. The workflow records the change itself once the work is on
|
|
717
750
|
the default branch. Every implement used to edit that one file, so two runs whose branches
|
|
718
751
|
were cut before the other merged conflicted on it and failed to open a pull request with the
|
|
@@ -728,12 +761,14 @@ timeout-minutes: 180
|
|
|
728
761
|
|
|
729
762
|
Choose exactly one:
|
|
730
763
|
|
|
731
|
-
- **`safeoutputs/create_pull_request`** , the normal path. Propose a pull request
|
|
732
|
-
|
|
764
|
+
- **`safeoutputs/create_pull_request`** , the normal path. Propose a pull request with the
|
|
765
|
+
verified changes. Do not pass `base`: the workflow sets it to the branch this run started
|
|
766
|
+
on, which is the branch you are working from, so a stack child lands on its feature branch
|
|
767
|
+
without you doing anything. The `body` **must close exactly this issue and nothing else**
|
|
733
768
|
(`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.
|
|
769
|
+
the branch you are on, the one named in step 1; do not invent another name. You do not
|
|
770
|
+
need to check whether a pull request already exists for this issue: the router does
|
|
771
|
+
that before dispatching you and does not start this workflow when one does.
|
|
737
772
|
- **`safeoutputs/report_incomplete`** , only when infrastructure or tooling prevents you
|
|
738
773
|
from completing the task, such as a pre-existing build failure you cannot fix. Provide a
|
|
739
774
|
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
|
|
@@ -420,6 +455,7 @@ jobs:
|
|
|
420
455
|
outputs:
|
|
421
456
|
valid: ${{ steps.validate.outputs.valid }}
|
|
422
457
|
outcome: ${{ steps.validate.outputs.outcome }}
|
|
458
|
+
pushed: ${{ steps.validate.outputs.pushed }}
|
|
423
459
|
steps:
|
|
424
460
|
- name: Checkout workflow actions
|
|
425
461
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -525,7 +561,7 @@ jobs:
|
|
|
525
561
|
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
562
|
${{ 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
563
|
- name: Merge approved pull request
|
|
528
|
-
if: needs.validate_output.outputs.outcome == 'auto-merge'
|
|
564
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root != 'true'
|
|
529
565
|
env:
|
|
530
566
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
531
567
|
REPO: ${{ github.repository }}
|
|
@@ -534,6 +570,20 @@ jobs:
|
|
|
534
570
|
set -euo pipefail
|
|
535
571
|
head_sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
536
572
|
gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
|
|
573
|
+
# The root's own gate run stops here: children may still be landing on the feature
|
|
574
|
+
# branch, and the merge decision for main belongs to a person. The comment says only
|
|
575
|
+
# what a reader needs: the gate held the root, children merge into the feature branch,
|
|
576
|
+
# and a human merges the root when the stack completes.
|
|
577
|
+
- name: Hold a stack root for a human merge
|
|
578
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root == 'true'
|
|
579
|
+
uses: ./.github/actions/create-issue-comment
|
|
580
|
+
with:
|
|
581
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
582
|
+
issue-number: ${{ needs.subject.outputs.issue }}
|
|
583
|
+
body: |
|
|
584
|
+
**Stack root held.** This pull request is the root of a stack: its children merge
|
|
585
|
+
into the feature branch, and it merges into ${{ github.event.repository.default_branch }} only when a person
|
|
586
|
+
does it, after all children have landed and the feature branch validates.
|
|
537
587
|
- name: Release remediated issue
|
|
538
588
|
if: needs.validate_output.outputs.outcome == 'remediated'
|
|
539
589
|
uses: ./.github/actions/remove-issue-labels
|
|
@@ -583,6 +633,22 @@ jobs:
|
|
|
583
633
|
token: ${{ steps.app-token.outputs.token }}
|
|
584
634
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
585
635
|
labels: ${{ env.WORKING_LABEL }}
|
|
636
|
+
# GitHub's closing keywords never fire when a PR merges into a branch other than the
|
|
637
|
+
# default, so a child PR landing on the feature branch leaves its issue open — and
|
|
638
|
+
# "zero open children" would never be reachable. This step closes the child issue
|
|
639
|
+
# deterministically, exactly the issue this PR implements, with the App token: the
|
|
640
|
+
# close raises routed events only for issue lifecycle the router already understands,
|
|
641
|
+
# and `gh issue close` on an already-closed issue is a harmless no-op, making the
|
|
642
|
+
# step safe to re-run.
|
|
643
|
+
- name: Close the child issue when the pull request landed on a feature branch
|
|
644
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.pr_base != '' && needs.subject.outputs.pr_base != github.event.repository.default_branch
|
|
645
|
+
env:
|
|
646
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
647
|
+
REPO: ${{ github.repository }}
|
|
648
|
+
ISSUE: ${{ needs.subject.outputs.issue }}
|
|
649
|
+
run: |
|
|
650
|
+
set -euo pipefail
|
|
651
|
+
gh issue close "$ISSUE" --repo "$REPO" --reason completed || true
|
|
586
652
|
# A person decides this one, so the screenshots come before the decision, in a router run of
|
|
587
653
|
# their own outside the belt. Never on auto-merge: that dispatch would race the merge.
|
|
588
654
|
- name: Replay the verification plan for the reviewer
|
|
@@ -718,6 +784,26 @@ jobs:
|
|
|
718
784
|
|
|
719
785
|
**Verdict:** human-review
|
|
720
786
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
787
|
+
# The issue is where the belt reads verdicts, but the pull request is where people look. A
|
|
788
|
+
# parked pull request with nothing on it reads as abandoned, and a repair the agent pushed
|
|
789
|
+
# that the gate refused to apply sits in the run artifact where nobody finds it.
|
|
790
|
+
- name: Tell the pull request it needs a person
|
|
791
|
+
if: steps.budget.outputs.park == 'true'
|
|
792
|
+
uses: ./.github/actions/create-issue-comment
|
|
793
|
+
with:
|
|
794
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
795
|
+
issue-number: ${{ needs.subject.outputs.pr }}
|
|
796
|
+
body: |
|
|
797
|
+
The merge gate stopped on this pull request and handed it to a person (#${{ needs.subject.outputs.issue }}).
|
|
798
|
+
${{ needs.validate_output.outputs.pushed == 'true' && 'The agent pushed a repair that was not applied because its report could not be read. The patch and bundle are in the `agent` artifact of the run below.' || '' }}
|
|
799
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
800
|
+
- name: Label the pull request for review
|
|
801
|
+
if: steps.budget.outputs.park == 'true'
|
|
802
|
+
uses: ./.github/actions/add-issue-labels
|
|
803
|
+
with:
|
|
804
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
805
|
+
issue-number: ${{ needs.subject.outputs.pr }}
|
|
806
|
+
labels: ${{ env.REVIEW_LABEL }}
|
|
721
807
|
# `stalled` means a park the machine caused, and the janitor retries those. An unusable
|
|
722
808
|
# report is a park the machine caused and retrying reproduces it, so it gets `review`
|
|
723
809
|
# alone: the janitor's own rule is retry a failure, report a decision.
|
|
@@ -914,6 +1000,13 @@ timeout-minutes: 120
|
|
|
914
1000
|
until it exits 0, then verify, push and report `remediated` as in step 6. An `assessed`
|
|
915
1001
|
verdict while it fails blocks the pull request.
|
|
916
1002
|
|
|
1003
|
+
When CI failed as well, repair CI first and `check-diff` second, and verify again after the
|
|
1004
|
+
`check-diff` repair. The two pull in opposite directions: a complexity or coverage gate wants
|
|
1005
|
+
more code and tests, `check-diff` wants fewer lines. Trimming to fit `check-diff` is how a
|
|
1006
|
+
pull request that once passed broke the formatter, Semgrep and the complexity gate on its
|
|
1007
|
+
last commit (Pliny-Bot #633). Never delete a test that covers a branch the change added; if
|
|
1008
|
+
both cannot hold, keep CI green and say so in `reason`.
|
|
1009
|
+
|
|
917
1010
|
- **success** → step 4, then step 5 (review the change).
|
|
918
1011
|
- **failure** → step 4, then step 6 (CI remediation).
|
|
919
1012
|
- **action_required, cancelled, timed_out, or anything else** → CI did not produce a usable
|
|
@@ -934,8 +1027,10 @@ timeout-minutes: 120
|
|
|
934
1027
|
4b. **Merge conflict when CI is green.** If the conclusion is `success` and
|
|
935
1028
|
`has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
|
|
936
1029
|
resolve the conflict before assessing risk. You are already on the PR branch.
|
|
937
|
-
Merge `origin/${{
|
|
938
|
-
(`git merge origin/${{
|
|
1030
|
+
Merge `origin/${{ needs.subject.outputs.pr_base }}` into it
|
|
1031
|
+
(`git merge origin/${{ needs.subject.outputs.pr_base }}`; that is the branch this pull
|
|
1032
|
+
request merges into — the default branch for a flat pull request, the feature branch
|
|
1033
|
+
for a stack child), resolve every
|
|
939
1034
|
conflict deliberately, commit the merge, and run the verification commands below. Do not
|
|
940
1035
|
use `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the
|
|
941
1036
|
intended behavior from both sides, and never rebase: the push is fast-forward only.
|
|
@@ -1098,8 +1193,9 @@ timeout-minutes: 120
|
|
|
1098
1193
|
must not auto-merge the PR.
|
|
1099
1194
|
|
|
1100
1195
|
**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
|
|
1196
|
+
it is not a reason to hand the PR to a human. Merge `origin/${{ needs.subject.outputs.pr_base }}`
|
|
1197
|
+
(the branch this pull request merges into) into the current branch, resolve every conflict
|
|
1198
|
+
deliberately, stage the resolutions, and
|
|
1103
1199
|
commit the merge. Then run verification and push the resulting branch update. Do not use
|
|
1104
1200
|
`--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
|
|
1105
1201
|
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.
|