@plainconceptsplatform/workflows 0.28.0 → 0.28.2

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.
@@ -1657,6 +1657,10 @@ echo "── Visual verify wiring ───────────────
1657
1657
  # The merge-gate's conclude job dispatches agent-visual-verify before merging when the outcome
1658
1658
  # is auto-merge and VISUAL_VERIFY_ENABLED is true. Each of these is one line an edit could drop
1659
1659
  # with nothing going red: a missing dispatch step, a missing env var, or a missing worker file.
1660
+ # The guard against the original bug comes first: the call is a dispatch, never a step doing
1661
+ # `uses:` on a workflow file -- that resolves as a local action, dies with "Can't find
1662
+ # action.yml", and under continue-on-error every auto-merge shipped with zero screenshots
1663
+ # and nothing red.
1660
1664
  if worker_installed merge-gate; then
1661
1665
  VV_OK=1
1662
1666
 
@@ -1665,14 +1669,27 @@ if worker_installed merge-gate; then
1665
1669
  }
1666
1670
 
1667
1671
  # The dispatch step exists and runs only on auto-merge with the feature flag on.
1668
- vv 'name: Run visual verification' "$MERGE_GATE_WORKER_MD" 'merge-gate has no visual-verify dispatch step'
1672
+ vv 'name: Dispatch visual verification' "$MERGE_GATE_WORKER_MD" 'merge-gate has no visual-verify dispatch step'
1669
1673
  vv "outcome == 'auto-merge'.*VISUAL_VERIFY_ENABLED == 'true'" "$MERGE_GATE_WORKER_MD" 'visual-verify dispatch has wrong guard'
1670
- vv 'continue-on-error: true' "$MERGE_GATE_WORKER_MD" 'visual-verify dispatch must not block the merge'
1674
+
1675
+ # The shape that never worked: a step calling a reusable workflow file. Also reject the
1676
+ # swallowed-failure form it shipped with.
1677
+ vv 'operation=visual-verify' "$MERGE_GATE_WORKER_MD" 'the dispatch must target the router operation, not a nested file'
1678
+ if grep -qE '^ - name:.*\n.*uses: \./\.github/workflows/agent-visual-verify' "$MERGE_GATE_WORKER_MD" 2>/dev/null \
1679
+ || grep -A2 '^ - name:' "$MERGE_GATE_WORKER_MD" | grep -q 'uses: \./\.github/workflows/'; then
1680
+ VV_OK=0
1681
+ echo "FAIL: a step still uses: a workflow file; the runner resolves it as a local action and it cannot work" >&2
1682
+ fi
1671
1683
 
1672
1684
  # The env vars the worker needs.
1673
1685
  vv 'VISUAL_VERIFY_ENABLED:' "$MERGE_GATE_WORKER_MD" 'merge-gate missing VISUAL_VERIFY_ENABLED env var'
1674
1686
  vv 'VISUAL_VERIFY_START_COMMAND:' "$MERGE_GATE_WORKER_MD" 'merge-gate missing VISUAL_VERIFY_START_COMMAND env var'
1675
1687
 
1688
+ # The worker gates its agent on its own configuration, so an unconfigured repository
1689
+ # runs no model: the subject job exposes it, the top-level if: reads it.
1690
+ vv 'enabled: ' "${WORKFLOWS_DIR}/agent-visual-verify.md" 'visual-verify subject does not expose the enabled output'
1691
+ vv "outputs.enabled == 'true'" "${WORKFLOWS_DIR}/agent-visual-verify.md" 'visual-verify agent gate does not read the enabled output'
1692
+
1676
1693
  # The worker file exists.
1677
1694
  VV_WORKER_MD="${WORKFLOWS_DIR}/agent-visual-verify.md"
1678
1695
  if [ ! -f "$VV_WORKER_MD" ]; then
@@ -2114,6 +2131,49 @@ if worker_installed implement; then
2114
2131
  if [ "$ENDINGS_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
2115
2132
  fi
2116
2133
 
2134
+ echo "── Refine size gate ──────────────────────────────────────────────────────"
2135
+
2136
+ # The size gate added a job that skips in the happy path, and gh-aw adds every custom job to
2137
+ # the agent's needs. Three claims about that shape are asserted, because each one broke in
2138
+ # production in the same week (Pliny-Bot #320-322: sixteen runs, three parked issues, a
2139
+ # refusal loop on an issue the author was shrinking live):
2140
+ #
2141
+ # 1. the agent's gate carries !failure(), so a skipped refusal no longer poisons the
2142
+ # implicit success() and kills the agent in 0s
2143
+ # 2. the incomplete job excludes the refusal, so a refused issue is terminal: no attempt
2144
+ # counter, no x/5, no re-dispatch, no stalled
2145
+ # 3. the refusal itself releases bot-working, so a refused issue is not left reserved
2146
+ if worker_installed refine; then
2147
+ SIZE_GATE_OK=1
2148
+ REFINE_WORKER_MD="${WORKFLOWS_DIR}/agent-refine.md"
2149
+
2150
+ # The top-level if: starts at column 0 -- job-level ifs: are indented -- so anchor on that,
2151
+ # not on the prose comment above it. It must end with !failure().
2152
+ top_level_if="$(sed -n 's/^if: //p' "$REFINE_WORKER_MD" | head -1)"
2153
+ if [[ "$top_level_if" != *'!failure()' ]]; then
2154
+ SIZE_GATE_OK=0
2155
+ echo "FAIL: refine gates the agent without !failure(); a skipped refuse_big_issue poisons the implicit success() and skips the agent on every under-limit issue" >&2
2156
+ fi
2157
+
2158
+ # Extract the incomplete job's own block. A whole-file grep cannot make this claim: reserve,
2159
+ # refuse_big_issue and the agent gate all read size_guard, so the exclusion clause could be
2160
+ # deleted from incomplete while every grep still passes.
2161
+ incomplete_block="$(awk '/^ incomplete:/{found=1; next} found && /^ [a-z_]+:/{exit} found{print}' "$REFINE_WORKER_MD")"
2162
+ if ! printf '%s' "$incomplete_block" | grep -qE '^ needs: \[.*size_guard' \
2163
+ || ! printf '%s' "$incomplete_block" | grep -qF "needs.size_guard.outputs.too_big != 'true'"; then
2164
+ SIZE_GATE_OK=0
2165
+ echo "FAIL: refine's incomplete does not exclude the refusal path; every refused issue also loops attempts" >&2
2166
+ fi
2167
+
2168
+ refuse_block="$(awk '/^ refuse_big_issue:/{found=1; next} found && /^ [a-z_]+:/{exit} found{print}' "$REFINE_WORKER_MD")"
2169
+ if ! printf '%s' "$refuse_block" | grep -qF 'WORKING_LABEL'; then
2170
+ SIZE_GATE_OK=0
2171
+ echo "FAIL: the refine refusal never releases bot-working; a refused issue stays reserved with no run behind it" >&2
2172
+ fi
2173
+
2174
+ if [ "$SIZE_GATE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
2175
+ fi
2176
+
2117
2177
  echo "── Runner pools ──────────────────────────────────────────────────────────"
2118
2178
 
2119
2179
  # Where every job runs, stated once and asserted, because GitHub gives a wrong pool no error: a
@@ -378,6 +378,17 @@ jobs:
378
378
  protected-hit: ${{ needs.protected_changes.outputs.requires_review }}
379
379
  owner-hit: ${{ needs.protected_changes.outputs.owner_hit }}
380
380
  confidence-threshold: ${{ env.CONFIDENCE_THRESHOLD }}
381
+ # The pre-merge screenshot pass is dispatched rather than nested. A nested
382
+ # workflow_call job cannot live downstream of this worker's agent: the compiler hoists
383
+ # prompt-referenced jobs into the agent's needs, and any call job depending on
384
+ # validate_output closes a cycle through agent (measured: the graph refuses to compile).
385
+ # The first version of this wiring was a step doing `uses:` on the worker's lock file,
386
+ # which is not a call at all -- the runner resolved it as a local action and died with
387
+ # "Can't find action.yml" under continue-on-error, so every auto-merge since the feature
388
+ # shipped merged with zero screenshots and nothing red. This dispatches the router's
389
+ # existing visual-verify operation (the dispatch-triage pattern) before the merge below.
390
+ # The merge pins the head SHA with --match-head-commit, and visual-verify re-reads the
391
+ # pull request itself, so the called worker owns its own subject validation.
381
392
  conclude:
382
393
  needs: [activation, subject, protected_changes, agent, safe_outputs, validate_output]
383
394
  # `protected_changes.result == 'success'` is stated rather than relied on. GitHub skips a job
@@ -396,6 +407,8 @@ jobs:
396
407
  contents: write
397
408
  issues: write
398
409
  pull-requests: write
410
+ # dispatching the visual-verify pass through the router's workflow_dispatch
411
+ actions: write
399
412
  steps:
400
413
  - name: Create bot token
401
414
  id: app-token
@@ -457,13 +470,30 @@ jobs:
457
470
  ```
458
471
 
459
472
  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 }})
460
- - name: Run visual verification
473
+ # Screenshots before the merge. The visual-verify pass is dispatched through the
474
+ # router (see the visual_verify note above conclude) and cannot be awaited from here;
475
+ # the merge below pins the head SHA with --match-head-commit, so a screenshot run on
476
+ # the same head describes exactly what is being merged. GITHUB_TOKEN: a dispatch
477
+ # raises one workflow_run-free event, and the called worker re-owns its subject.
478
+ - name: Dispatch visual verification
461
479
  if: needs.validate_output.outputs.outcome == 'auto-merge' && env.VISUAL_VERIFY_ENABLED == 'true'
462
- uses: ./.github/workflows/agent-visual-verify.lock.yml
463
- with:
464
- pr-number: ${{ needs.subject.outputs.pr }}
465
- linked-issue: ${{ needs.subject.outputs.issue }}
466
- continue-on-error: true
480
+ env:
481
+ GH_TOKEN: ${{ github.token }}
482
+ REPO: ${{ github.repository }}
483
+ REF: ${{ github.event.repository.default_branch }}
484
+ PR: ${{ needs.subject.outputs.pr }}
485
+ ISSUE: ${{ needs.subject.outputs.issue }}
486
+ run: |
487
+ set -euo pipefail
488
+ if gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
489
+ -f operation=visual-verify -f pr-number="$PR" -f issue-number="$ISSUE"; then
490
+ echo "Visual verification dispatched for PR #$PR."
491
+ else
492
+ echo "::warning::could not dispatch visual verification for PR #$PR; merging without screenshots."
493
+ fi
494
+ # The visual verification pass is the visual_verify dispatch above. It runs as its own
495
+ # worker because a step cannot call a reusable workflow, and the merge below may land
496
+ # while the capture is still running -- the head SHA pin keeps the two honest.
467
497
  - name: Merge approved pull request
468
498
  if: needs.validate_output.outputs.outcome == 'auto-merge'
469
499
  env:
@@ -229,6 +229,17 @@ jobs:
229
229
  token: ${{ steps.app-token.outputs.token }}
230
230
  issue-number: ${{ inputs.issue-number }}
231
231
  labels: stalled
232
+ # A refusal must also release the reservation. authorize-bot-work adds bot-working
233
+ # before the router dispatches, and this job is the refused run's only terminal path:
234
+ # incomplete is gated off it (see there), so without this removal a refused issue
235
+ # stays reserved with no run behind it -- parked, invisible, not re-triggerable until
236
+ # the hourly reconcile sweep clears it hours later.
237
+ - name: Release the reservation
238
+ uses: ./.github/actions/remove-issue-labels
239
+ with:
240
+ token: ${{ steps.app-token.outputs.token }}
241
+ issue-number: ${{ inputs.issue-number }}
242
+ labels: ${{ env.WORKING_LABEL }}
232
243
  reserve:
233
244
  needs: [still_open, size_guard]
234
245
  if: needs.still_open.outputs.open == 'true' && needs.size_guard.outputs.too_big != 'true'
@@ -475,9 +486,15 @@ jobs:
475
486
  incomplete:
476
487
  # activation for the artifact prefix the usage read needs; validate_output for its
477
488
  # valid output, which decides whether the usage read should look for truncation at all.
478
- needs: [activation, agent, safe_outputs, validate_output]
489
+ # size_guard because a refusal is terminal: when too_big is true, refuse_big_issue has
490
+ # already spoken on the issue and no attempt, retry or park may follow. Without this
491
+ # clause every refused issue also looped here -- one human-label visit produced eight
492
+ # refusal comments and eight "Attempt N of 5" comments, none of which could succeed
493
+ # (Pliny-Bot #322).
494
+ needs: [activation, agent, safe_outputs, validate_output, size_guard]
479
495
  if: >
480
496
  always() &&
497
+ needs.size_guard.outputs.too_big != 'true' &&
481
498
  (
482
499
  needs.agent.result != 'success' ||
483
500
  needs.safe_outputs.result != 'success' ||
@@ -598,7 +615,15 @@ jobs:
598
615
  ${{ steps.usage.outputs.truncated }}
599
616
  [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
600
617
 
601
- if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true' && needs.size_guard.outputs.too_big != 'true'
618
+ # `!failure()` is load-bearing, not decoration. gh-aw adds every custom job to the agent's
619
+ # `needs`, including refuse_big_issue -- a job that skips precisely when refinement should
620
+ # run. Without a status function in this `if:` GitHub prepends an implicit `success()`,
621
+ # and a skipped need fails it, so every under-limit issue skipped the agent in 0s and the
622
+ # incomplete loop parked the issue (Pliny-Bot #320-322, 16 runs). `!failure()` suppresses
623
+ # the implicit success(): a refused skip no longer poisons the agent, while any real
624
+ # failure -- size_guard erroring, the refusal job erroring mid-refuse, reserve failing --
625
+ # still blocks it and lands in incomplete.
626
+ if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true' && needs.size_guard.outputs.too_big != 'true' && !failure()
602
627
 
603
628
  runs-on: agents-arc
604
629
  runs-on-slim: agents-arc
@@ -753,16 +778,16 @@ timeout-minutes: 90
753
778
  The visible line is for people and the marker is read by the workflow, which turns it into the
754
779
  `sp-N` label. A body without the marker gets no estimate label at all.
755
780
 
756
- 7. **One safe-output call per turn.** Never batch multiple safe-output calls into a single
757
- message: `update_issue`, `create_issue` and `add_comment` each go in their own turn, with
758
- nothing else in the message. The provider caps one response at a fixed size, and a batch of
759
- large calls is truncated mid-JSON before any of them executes, ending the run green with
760
- nothing written. On the split path, sequence `create_issue` → `create_issue` → … →
761
- `update_issue` → `add_comment`, one per turn. If a single body is so large it approaches the
762
- size of a very long message, tighten the body; a shorter call that lands beats a longer one
763
- that is cut off.
781
+ 7. **One safe-output call per turn.** Never batch multiple safe-output calls into a single
782
+ message: `update_issue`, `create_issue` and `add_comment` each go in their own turn, with
783
+ nothing else in the message. The provider caps one response at a fixed size, and a batch of
784
+ large calls is truncated mid-JSON before any of them executes, ending the run green with
785
+ nothing written. On the split path, sequence `create_issue` → `create_issue` → … →
786
+ `update_issue` → `add_comment`, one per turn. If a single body is so large it approaches the
787
+ size of a very long message, tighten the body; a shorter call that lands beats a longer one
788
+ that is cut off.
764
789
 
765
- 8. Decide exactly one outcome:
790
+ 8. Decide exactly one outcome:
766
791
 
767
792
  Labels are workflow-owned state. Do not call `add_labels` or `remove_labels`.
768
793
 
@@ -43,6 +43,10 @@ on:
43
43
  description: Issue number the pull request closes.
44
44
  required: true
45
45
  type: string
46
+ # The gate job the top-level `if:` reads. gh-aw folds that `if:` into the generated
47
+ # activation job but gives activation no dependency on the job, so the reference resolves
48
+ # to '' and the clause is false -- the agent would never run.
49
+ needs: [subject]
46
50
 
47
51
  # Rung 3-4. The agent reads the verification plan and writes waypoints as JSON; a post-agent
48
52
  # shell step runs agent-browser to capture screenshots outside the awf sandbox.
@@ -57,11 +61,28 @@ jobs:
57
61
  found: ${{ steps.subject.outputs.found }}
58
62
  pr: ${{ steps.subject.outputs.pr }}
59
63
  issue: ${{ steps.subject.outputs.issue }}
64
+ enabled: ${{ steps.config.outputs.enabled }}
60
65
  steps:
61
66
  - name: Checkout workflow actions
62
67
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
63
68
  with:
64
69
  persist-credentials: false
70
+ # Rung 4. A step can read the env: context; a job-level if: cannot. The enablement
71
+ # check therefore happens here and the agent gate reads the output -- a runnable
72
+ # snippet on an unconfigured repository burns the model otherwise.
73
+ - name: Read the visual-verify configuration
74
+ id: config
75
+ env:
76
+ ENABLED: ${{ env.VISUAL_VERIFY_ENABLED }}
77
+ START_COMMAND: ${{ env.VISUAL_VERIFY_START_COMMAND }}
78
+ run: |
79
+ set -euo pipefail
80
+ if [ "$ENABLED" = "true" ] && [ -n "$START_COMMAND" ]; then
81
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
82
+ else
83
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
84
+ echo "::notice::visual verification is not configured (VISUAL_VERIFY_ENABLED=$ENABLED, start command ${START_COMMAND:-unset}); the agent will not run."
85
+ fi
65
86
  - name: Identify the pull request and its issue
66
87
  id: subject
67
88
  env:
@@ -80,7 +101,7 @@ jobs:
80
101
  issue="$(jq -r '.closingIssuesReferences[0].number // empty' <<<"$pr")"
81
102
  fi
82
103
  if [ -z "$issue" ]; then
83
- issue="$(jq -r '.body // ""' <<<"$pr" |
104
+ issue="$(jq -r '.body // ""' <<<"$pr")"
84
105
  grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' |
85
106
  grep -oE '[0-9]+' | head -n 1 || true)"
86
107
  fi
@@ -198,6 +219,11 @@ post-steps:
198
219
  run: |
199
220
  kill "$APP_PID" 2>/dev/null || true
200
221
 
222
+ # `!failure()` because subject skips on an unconfigured repository and a closed pull
223
+ # request: without a status function the implicit success() would let those skips look
224
+ # like failures to every job that needs this one.
225
+ if: needs.subject.outputs.found == 'true' && needs.subject.outputs.enabled == 'true' && !failure()
226
+
201
227
  timeout-minutes: 15
202
228
 
203
229
  engine:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",