@plainconceptsplatform/workflows 1.3.0 → 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/apply-agent-output/action.yml +113 -108
- package/loops/actions/create-agent-issues/action.yml +113 -52
- package/loops/actions/housekeeping/action.yml +95 -1
- package/loops/actions/validate-merge-gate-output/action.yml +6 -1
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +9 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +30 -1
- package/loops/actions/verify-refine-output/verify-refine-output.sh +17 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +224 -9
- package/loops/docs/diagrams.md +90 -12
- package/loops/scripts/compile-agent-workflows.mjs +76 -2
- package/loops/workflows/agent-apply-review.md +5 -0
- package/loops/workflows/agent-implement.md +43 -7
- package/loops/workflows/agent-merge-gate.md +136 -8
- package/loops/workflows/agent-refine.md +152 -5
- package/loops/workflows/work-router.yml +330 -2
- package/package.json +1 -1
|
@@ -342,6 +342,13 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
342
342
|
// pull request as an issue instead of a pull request). Checking out the branch tip at
|
|
343
343
|
// execution time makes the commit parent current main; the implement-global queue
|
|
344
344
|
// already keeps sibling merges out of the window while the agent runs.
|
|
345
|
+
//
|
|
346
|
+
// A stack child dispatches this workflow directly (`gh workflow run
|
|
347
|
+
// agent-implement.lock.yml --ref feat/N`), and there the pin must select the feature
|
|
348
|
+
// branch tip, not main: github.ref_name is the ref the dispatch named. Under
|
|
349
|
+
// workflow_call the callee inherits the caller's event, and the router is only ever
|
|
350
|
+
// dispatched on the default branch, so the dispatch arm of the ternary resolves to
|
|
351
|
+
// main there too -- flat behavior is byte-identical.
|
|
345
352
|
if (file === "agent-implement.lock.yml") {
|
|
346
353
|
const start = rewritten.indexOf("\n agent:\n");
|
|
347
354
|
const rest = start === -1 ? "" : rewritten.slice(start + 1);
|
|
@@ -351,12 +358,56 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
351
358
|
const span = rewritten.slice(start, end).replace(
|
|
352
359
|
/(uses: actions\/checkout@[^\n]*\n(\s+)with:\n(?:\2 [^\n]+\n)*)/,
|
|
353
360
|
(whole, _block, indent) =>
|
|
354
|
-
whole.includes("ref:")
|
|
361
|
+
whole.includes("ref:")
|
|
362
|
+
? whole
|
|
363
|
+
: whole +
|
|
364
|
+
indent +
|
|
365
|
+
" ref: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.repository.default_branch }}\n",
|
|
355
366
|
);
|
|
356
367
|
rewritten = rewritten.slice(0, start) + span + rewritten.slice(end);
|
|
357
368
|
}
|
|
358
369
|
}
|
|
359
370
|
|
|
371
|
+
// update_issue declares no required field, so the safe-outputs MCP server accepts a call
|
|
372
|
+
// carrying only issue_number and answers success. An agent that drops the body - the
|
|
373
|
+
// documented way is omitting the trailing `.` so the bridge never reads stdin - loses its
|
|
374
|
+
// whole story, is told nothing, and goes on to claim completion. add_comment declares body
|
|
375
|
+
// required and rejects the identical mistake, which is how the agent recovers from it on
|
|
376
|
+
// the next turn. Measured on Lyceum before this: one refine run in five wrote its story,
|
|
377
|
+
// the other four ended green with the issue untouched (#44, #59 twice, #62), and Numa #698
|
|
378
|
+
// reached `refined, implement` still carrying its original form body.
|
|
379
|
+
//
|
|
380
|
+
// Only agent-refine calls update_issue, and always to replace the body; labels and status
|
|
381
|
+
// are workflow-owned and the prompt forbids the agent from touching them, so requiring the
|
|
382
|
+
// body costs no legitimate call. The check below fails the compile if this stops matching.
|
|
383
|
+
{
|
|
384
|
+
const toolStart = rewritten.indexOf('"update_issue": {');
|
|
385
|
+
if (toolStart !== -1) {
|
|
386
|
+
const bodyField = rewritten.indexOf('"body": {', toolStart);
|
|
387
|
+
if (bodyField !== -1) {
|
|
388
|
+
const insertAt = bodyField + '"body": {'.length;
|
|
389
|
+
if (!rewritten.slice(insertAt, insertAt + 80).includes('"required": true')) {
|
|
390
|
+
rewritten = rewritten.slice(0, insertAt) + '\n' + ' '.repeat(20) + '"required": true,' + rewritten.slice(insertAt);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The pull request base belongs to the handler, never to the MCP server. agent-implement sets
|
|
397
|
+
// base-branch from the dispatched ref so a stack child lands on its feature branch, and gh-aw
|
|
398
|
+
// writes that base into both configs. The safe-outputs MCP server runs inside the sandbox,
|
|
399
|
+
// where remote refs are not visible, and given a base it insists on a merge-base against
|
|
400
|
+
// origin/<base>: on Lyceum #73 it answered "No remote refs available for merge-base
|
|
401
|
+
// calculation" twice and the run died at max-turns with the code already written (run
|
|
402
|
+
// 38737284343). With no base it patches from the pinned SHA, which the stack dispatch sets to
|
|
403
|
+
// the feature tip, and that is exactly the patch the handler then applies onto the base. So
|
|
404
|
+
// the MCP config loses the base and the handler config keeps it. The check below fails the
|
|
405
|
+
// compile if this stops matching.
|
|
406
|
+
rewritten = rewritten.replace(
|
|
407
|
+
/^([ \t]+GH_AW_SAFE_OUTPUTS_CONFIG: "[^\n]*?)\\"base_branch\\":\\"[^\\"]*\\",/m,
|
|
408
|
+
"$1",
|
|
409
|
+
);
|
|
410
|
+
|
|
360
411
|
// Guard the two rewrites the compiled agent jobs cannot work without. The anchors are
|
|
361
412
|
// upstream template text: when a gh-aw upgrade reshapes either step, the silent
|
|
362
413
|
// failure mode returns (an artifact with no bundle, a skipped push, a green gate on
|
|
@@ -365,8 +416,31 @@ for (const file of readdirSync(workflowDirectory)) {
|
|
|
365
416
|
{ name: "bundle upload glob", ok: /\n( +)- name: Upload agent artifacts\n\1 if: always\(\)\n\1 continue-on-error: true\n\1 uses: actions\/upload-artifact@[^\n]*\n\1 with:\n\1 name: \$\{\{ needs\.activation\.outputs\.artifact_prefix \}\}agent\n\1 path: \|\n(?:\1 [^\n]*\n)*\1 \/tmp\/gh-aw\/aw-\*\.bundle\n/ },
|
|
366
417
|
{ name: "placeholder step intact", ok: /\n( +)- name: Write agent output placeholder if missing\n\1 if: always\(\)\n\1 run: \|\n\1 if \[ ! -f \/tmp\/gh-aw\/agent_output\.json \]; then\n\1 echo '\{"items":\[\]\}' > \/tmp\/gh-aw\/agent_output\.json\n\1 fi\n/ },
|
|
367
418
|
{ name: "failed-jobs reporter disabled", ok: /- name: Report failed jobs\n[ \t]*id: report_failed_jobs\n[ \t]*if: false[^\n]*\n/ },
|
|
419
|
+
// Only agent-refine declares update_issue; the other workers never carry the tool.
|
|
420
|
+
{
|
|
421
|
+
name: "update_issue requires a body",
|
|
422
|
+
ok: lock =>
|
|
423
|
+
!lock.includes('"update_issue": {') ||
|
|
424
|
+
/"update_issue": \{[\s\S]{0,3000}?"body": \{\s*\n\s*"required": true,/.test(lock),
|
|
425
|
+
},
|
|
426
|
+
// The base reaches the handler and never the MCP server; see the rewrite above. Anchored on
|
|
427
|
+
// the escaped quotes because allowed_base_branches contains the bare text base_branch.
|
|
428
|
+
{
|
|
429
|
+
name: "pull request base stays out of the MCP config",
|
|
430
|
+
ok: lock => {
|
|
431
|
+
const mcp = (lock.match(/^[ \t]+GH_AW_SAFE_OUTPUTS_CONFIG: "[^\n]*/m) || [""])[0];
|
|
432
|
+
const handler = (lock.match(/^[ \t]+GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "[^\n]*/m) || [""])[0];
|
|
433
|
+
if (mcp.includes('\\"base_branch\\"')) return false;
|
|
434
|
+
// The handler config keeps the expression for Actions to expand before the job starts;
|
|
435
|
+
// only the MCP config is rewritten to an env reference. Either form is the run's ref.
|
|
436
|
+
return !file.startsWith("agent-implement") ||
|
|
437
|
+
handler.includes('\\"base_branch\\":\\"${{ github.ref_name }}\\"') ||
|
|
438
|
+
handler.includes('\\"base_branch\\":\\"${GITHUB_REF_NAME}\\"');
|
|
439
|
+
},
|
|
440
|
+
},
|
|
368
441
|
]) {
|
|
369
|
-
|
|
442
|
+
const passed = typeof check.ok === "function" ? check.ok(rewritten) : check.ok.test(rewritten);
|
|
443
|
+
if (!passed) {
|
|
370
444
|
process.stderr.write(
|
|
371
445
|
`compile patch "${check.name}" no longer matches in ${file}: the gh-aw template changed. ` +
|
|
372
446
|
`Re-anchor the rewrite in loops/scripts/compile-agent-workflows.mjs, or agent pushes are silently dropped.\n`,
|
|
@@ -541,6 +541,11 @@ timeout-minutes: 90
|
|
|
541
541
|
${{ env.VERIFY_COMMANDS }}
|
|
542
542
|
```
|
|
543
543
|
|
|
544
|
+
When `.opencode/scripts/check-diff.mjs` exists, also run
|
|
545
|
+
`node .opencode/scripts/check-diff.mjs`.
|
|
546
|
+
It must exit 0: delete the comments and tests it lists. The merge gate runs it too and does not
|
|
547
|
+
merge while it fails.
|
|
548
|
+
|
|
544
549
|
7. Select one review outcome.
|
|
545
550
|
|
|
546
551
|
- **implemented**: You made the requested change, verification passed, and you will propose
|
|
@@ -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
|
|
@@ -707,6 +735,12 @@ timeout-minutes: 180
|
|
|
707
735
|
|
|
708
736
|
Never open a pull request that does not pass them.
|
|
709
737
|
|
|
738
|
+
When `.opencode/scripts/check-diff.mjs` exists, also run
|
|
739
|
+
`node .opencode/scripts/check-diff.mjs`
|
|
740
|
+
on both paths. It must exit 0. It holds the comments and tests you add to the size of the
|
|
741
|
+
change and makes you clean the files you touched; fix every line it lists. The merge gate
|
|
742
|
+
runs it again and does not merge while it fails.
|
|
743
|
+
|
|
710
744
|
5. Do not touch `changelog.json`. The workflow records the change itself once the work is on
|
|
711
745
|
the default branch. Every implement used to edit that one file, so two runs whose branches
|
|
712
746
|
were cut before the other merged conflicted on it and failed to open a pull request with the
|
|
@@ -722,12 +756,14 @@ timeout-minutes: 180
|
|
|
722
756
|
|
|
723
757
|
Choose exactly one:
|
|
724
758
|
|
|
725
|
-
- **`safeoutputs/create_pull_request`** , the normal path. Propose a pull request
|
|
726
|
-
|
|
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**
|
|
727
763
|
(`Closes #${{ inputs.issue-number }}`) and summarise what changed and why. Its `branch` is
|
|
728
|
-
the branch you are on, the one named in step 1; do not invent another name. You do not
|
|
729
|
-
to check whether a pull request already exists for this issue: the router does
|
|
730
|
-
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.
|
|
731
767
|
- **`safeoutputs/report_incomplete`** , only when infrastructure or tooling prevents you
|
|
732
768
|
from completing the task, such as a pre-existing build failure you cannot fix. Provide a
|
|
733
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"
|
|
@@ -131,7 +133,9 @@ on:
|
|
|
131
133
|
# this entry activation read needs.protected_changes.outputs.requires_review before
|
|
132
134
|
# protected_changes had started; the value was '' and the clause was always true
|
|
133
135
|
# (Pliny-Bot run 34042143350: activation finished 18 s before protected_changes began).
|
|
134
|
-
|
|
136
|
+
# code_rules is here for the same reason: the prompt prints its result and report, and the
|
|
137
|
+
# prompt is rendered in activation.
|
|
138
|
+
needs: [protected_changes, code_rules]
|
|
135
139
|
|
|
136
140
|
# Rung 4. Router has classified the event; identify-gate-subject validates PR ownership,
|
|
137
141
|
# resolves the closing issue, and confirms the CI verdict.
|
|
@@ -153,6 +157,8 @@ jobs:
|
|
|
153
157
|
conclusion: ${{ steps.subject.outputs.conclusion }}
|
|
154
158
|
run-id: ${{ steps.subject.outputs.run-id }}
|
|
155
159
|
review_blocked: ${{ steps.review.outputs.review_blocked }}
|
|
160
|
+
pr_base: ${{ steps.stack.outputs.pr_base }}
|
|
161
|
+
stack_root: ${{ steps.stack.outputs.stack_root }}
|
|
156
162
|
steps:
|
|
157
163
|
- name: Checkout workflow actions
|
|
158
164
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -187,6 +193,37 @@ jobs:
|
|
|
187
193
|
sleep 5
|
|
188
194
|
done
|
|
189
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"
|
|
190
227
|
|
|
191
228
|
# Rung 4b. Everything the merge decision needs that a shell can establish, measured once,
|
|
192
229
|
# before any model reads the diff. The job kept its name and its `requires_review` and
|
|
@@ -240,6 +277,51 @@ jobs:
|
|
|
240
277
|
medium-lines: ${{ env.BLAST_MEDIUM_LINES }}
|
|
241
278
|
ignore-paths: ${{ env.BLAST_IGNORE_PATHS }}
|
|
242
279
|
|
|
280
|
+
# Rung 4c. The comments and tests the change adds, measured by the harness's check-diff on the
|
|
281
|
+
# pull request head before any model reads it. Prose in the implement prompt did not hold bot
|
|
282
|
+
# pull requests to the guardrails: they added 27-63% comment lines in October 2026. `fail`
|
|
283
|
+
# stops an auto-merge in the validator; the agent is told to repair it like a CI failure.
|
|
284
|
+
# A repository without the script reports `absent` and is not held to it.
|
|
285
|
+
code_rules:
|
|
286
|
+
needs: subject
|
|
287
|
+
if: needs.subject.outputs.found == 'true'
|
|
288
|
+
runs-on: agents-arc
|
|
289
|
+
timeout-minutes: 10
|
|
290
|
+
permissions:
|
|
291
|
+
contents: read
|
|
292
|
+
outputs:
|
|
293
|
+
result: ${{ steps.check.outputs.result }}
|
|
294
|
+
report: ${{ steps.check.outputs.report }}
|
|
295
|
+
steps:
|
|
296
|
+
- name: Checkout the pull request
|
|
297
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
298
|
+
with:
|
|
299
|
+
ref: refs/pull/${{ needs.subject.outputs.pr }}/head
|
|
300
|
+
fetch-depth: 0
|
|
301
|
+
persist-credentials: false
|
|
302
|
+
- name: Check the comments and tests it adds
|
|
303
|
+
id: check
|
|
304
|
+
env:
|
|
305
|
+
BASE: origin/${{ github.event.repository.default_branch || 'main' }}
|
|
306
|
+
run: |
|
|
307
|
+
set -uo pipefail
|
|
308
|
+
if [ ! -f .opencode/scripts/check-diff.mjs ]; then
|
|
309
|
+
echo "result=absent" >> "$GITHUB_OUTPUT"
|
|
310
|
+
echo "check-diff is not installed (agent-harness 3.2.0 or later ships it), so comments and tests were not checked." >> "$GITHUB_STEP_SUMMARY"
|
|
311
|
+
exit 0
|
|
312
|
+
fi
|
|
313
|
+
report=$(node .opencode/scripts/check-diff.mjs --base "$BASE" 2>&1)
|
|
314
|
+
code=$?
|
|
315
|
+
case "$code" in 0) result=pass ;; 1) result=fail ;; *) result=error ;; esac
|
|
316
|
+
delimiter="EOF_$(openssl rand -hex 8)"
|
|
317
|
+
{
|
|
318
|
+
echo "result=$result"
|
|
319
|
+
echo "report<<$delimiter"
|
|
320
|
+
printf '%s\n' "${report:0:8000}"
|
|
321
|
+
echo "$delimiter"
|
|
322
|
+
} >> "$GITHUB_OUTPUT"
|
|
323
|
+
printf '```\n%s\n```\n' "$report" >> "$GITHUB_STEP_SUMMARY"
|
|
324
|
+
|
|
243
325
|
review_required:
|
|
244
326
|
needs: [subject, protected_changes]
|
|
245
327
|
# gh-aw makes the agent depend on custom jobs. Keep this job successful when
|
|
@@ -362,7 +444,7 @@ jobs:
|
|
|
362
444
|
Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
|
|
363
445
|
Bot is working on fixing it.
|
|
364
446
|
validate_output:
|
|
365
|
-
needs: [activation, subject, protected_changes, agent, safe_outputs]
|
|
447
|
+
needs: [activation, subject, protected_changes, code_rules, agent, safe_outputs]
|
|
366
448
|
if: >
|
|
367
449
|
always() &&
|
|
368
450
|
needs.agent.result == 'success' &&
|
|
@@ -396,6 +478,7 @@ jobs:
|
|
|
396
478
|
protected-hit: ${{ needs.protected_changes.outputs.requires_review }}
|
|
397
479
|
owner-hit: ${{ needs.protected_changes.outputs.owner_hit }}
|
|
398
480
|
confidence-threshold: ${{ env.CONFIDENCE_THRESHOLD }}
|
|
481
|
+
code-rules: ${{ needs.code_rules.outputs.result }}
|
|
399
482
|
conclude:
|
|
400
483
|
needs: [activation, subject, protected_changes, agent, safe_outputs, validate_output]
|
|
401
484
|
# `protected_changes.result == 'success'` is stated rather than relied on. GitHub skips a job
|
|
@@ -477,7 +560,7 @@ jobs:
|
|
|
477
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 }})
|
|
478
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.' || '' }}
|
|
479
562
|
- name: Merge approved pull request
|
|
480
|
-
if: needs.validate_output.outputs.outcome == 'auto-merge'
|
|
563
|
+
if: needs.validate_output.outputs.outcome == 'auto-merge' && needs.subject.outputs.stack_root != 'true'
|
|
481
564
|
env:
|
|
482
565
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
483
566
|
REPO: ${{ github.repository }}
|
|
@@ -486,6 +569,20 @@ jobs:
|
|
|
486
569
|
set -euo pipefail
|
|
487
570
|
head_sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
488
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.
|
|
489
586
|
- name: Release remediated issue
|
|
490
587
|
if: needs.validate_output.outputs.outcome == 'remediated'
|
|
491
588
|
uses: ./.github/actions/remove-issue-labels
|
|
@@ -535,6 +632,22 @@ jobs:
|
|
|
535
632
|
token: ${{ steps.app-token.outputs.token }}
|
|
536
633
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
537
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
|
|
538
651
|
# A person decides this one, so the screenshots come before the decision, in a router run of
|
|
539
652
|
# their own outside the belt. Never on auto-merge: that dispatch would race the merge.
|
|
540
653
|
- name: Replay the verification plan for the reviewer
|
|
@@ -854,6 +967,18 @@ timeout-minutes: 120
|
|
|
854
967
|
${{ needs.protected_changes.outputs.signals }}
|
|
855
968
|
```
|
|
856
969
|
|
|
970
|
+
**Comments and tests (`check-diff`): `${{ needs.code_rules.outputs.result }}`**
|
|
971
|
+
|
|
972
|
+
```
|
|
973
|
+
${{ needs.code_rules.outputs.report }}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
When that is `fail`, repairing it is part of this run whatever CI concluded: delete the
|
|
977
|
+
comments and tests it lists, leave every file it names with no slop, run
|
|
978
|
+
`node .opencode/scripts/check-diff.mjs`
|
|
979
|
+
until it exits 0, then verify, push and report `remediated` as in step 6. An `assessed`
|
|
980
|
+
verdict while it fails blocks the pull request.
|
|
981
|
+
|
|
857
982
|
- **success** → step 4, then step 5 (review the change).
|
|
858
983
|
- **failure** → step 4, then step 6 (CI remediation).
|
|
859
984
|
- **action_required, cancelled, timed_out, or anything else** → CI did not produce a usable
|
|
@@ -874,8 +999,10 @@ timeout-minutes: 120
|
|
|
874
999
|
4b. **Merge conflict when CI is green.** If the conclusion is `success` and
|
|
875
1000
|
`has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
|
|
876
1001
|
resolve the conflict before assessing risk. You are already on the PR branch.
|
|
877
|
-
Merge `origin/${{
|
|
878
|
-
(`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
|
|
879
1006
|
conflict deliberately, commit the merge, and run the verification commands below. Do not
|
|
880
1007
|
use `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the
|
|
881
1008
|
intended behavior from both sides, and never rebase: the push is fast-forward only.
|
|
@@ -1038,8 +1165,9 @@ timeout-minutes: 120
|
|
|
1038
1165
|
must not auto-merge the PR.
|
|
1039
1166
|
|
|
1040
1167
|
**If `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`):** You are already on the PR branch. Resolve the conflict;
|
|
1041
|
-
it is not a reason to hand the PR to a human. Merge `origin/${{
|
|
1042
|
-
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
|
|
1043
1171
|
commit the merge. Then run verification and push the resulting branch update. Do not use
|
|
1044
1172
|
`--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
|
|
1045
1173
|
behavior from both sides.
|