@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.
@@ -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:") ? whole : whole + indent + " ref: ${{ github.event.repository.default_branch }}\n",
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
- if (!check.ok.test(rewritten)) {
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-only worker: triggered exclusively via workflow_call from work-router.yml.
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 against
726
- `main` with the verified changes. Its `body` must close the issue
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 need
729
- to check whether a pull request already exists for this issue: the router does that before
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
- The router supplies the CI conclusion and run ID as facts, so there is no polling and no
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
- needs: [protected_changes]
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/${{ github.event.repository.default_branch }}` into it
878
- (`git merge origin/${{ github.event.repository.default_branch }}`), resolve every
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/${{ github.event.repository.default_branch }}`
1042
- into the current branch, resolve every conflict deliberately, stage the resolutions, and
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.