@plainconceptsplatform/workflows 1.0.14 → 1.1.1

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.
@@ -0,0 +1,352 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/evidence.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: "Agentics: Evidence"
3
+
4
+ # Archives what a merged pull request looks like. The agent that implemented the change wrote a
5
+ # verification plan with the harness skill pc-repo-verify; this replays that plan against the
6
+ # app, with no model, and keeps one screenshot per waypoint on the pull request. The router calls
7
+ # it when a pull request merges, and operation=evidence runs it by hand for any pull request.
8
+ # Nothing waits on it and nothing reads it: it is the record, not a gate.
9
+ #
10
+ # Two jobs, because one runs the pull request's code and the other writes. `capture` checks out
11
+ # the head, starts the app and drives Chromium with a read-only token and no secret. `publish`
12
+ # checks nothing out: it takes the images as an artifact, commits them to the orphan `evidence`
13
+ # branch and writes one comment. Both use GITHUB_TOKEN, so a repository configures nothing, and
14
+ # nothing either job writes raises another workflow event.
15
+ #
16
+ # This replaced agent-visual-verify, which never produced a screenshot: it ran only on pull
17
+ # requests nobody would review, raced the merge it was dispatched before, re-planned with a model
18
+ # what implement had already planned, and published through `gh --attach`, which refuses the
19
+ # installation tokens a workflow has.
20
+
21
+ on:
22
+ workflow_call:
23
+ inputs:
24
+ pr-number:
25
+ description: Pull request whose verification plans are replayed.
26
+ required: true
27
+ type: string
28
+
29
+ env:
30
+ # The command that brings the whole stack up with mock auth. Every Platform repository has it at
31
+ # the root; a plan that needs something else names it in its own `start:`.
32
+ EVIDENCE_START_COMMAND: "pnpm run dev"
33
+ # How long the app may take to answer before the run records that it never did. A cold .NET
34
+ # build plus SQL Server in a container is most of it.
35
+ EVIDENCE_START_TIMEOUT_SECONDS: "900"
36
+ # The SDK when a repository has .NET projects but no global.json to say which.
37
+ EVIDENCE_DOTNET_VERSION: "10.0.x"
38
+ EVIDENCE_BRANCH: evidence
39
+ EVIDENCE_MARKER: "<!-- agentic-evidence -->"
40
+ # The capture runs in mcr.microsoft.com/playwright at this tag, pre-pulled by the runner image
41
+ # (runners/versions.env), so the two move together.
42
+ PLAYWRIGHT_VERSION: "1.62.1"
43
+
44
+ jobs:
45
+ capture:
46
+ runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
47
+ timeout-minutes: 45
48
+ permissions:
49
+ contents: read
50
+ pull-requests: read
51
+ outputs:
52
+ has-evidence: ${{ steps.done.outputs.has-evidence }}
53
+ steps:
54
+ - name: Checkout workflow actions
55
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
56
+ with:
57
+ persist-credentials: false
58
+
59
+ - name: Find the pull request's verification plans
60
+ id: plans
61
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
62
+ env:
63
+ PR_NUMBER: ${{ inputs.pr-number }}
64
+ with:
65
+ script: |
66
+ const pull_number = Number(process.env.PR_NUMBER);
67
+ const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number });
68
+ const files = await github.paginate(github.rest.pulls.listFiles, { ...context.repo, pull_number, per_page: 100 });
69
+ const plans = files
70
+ .filter((file) => file.status !== 'removed' && /(^|\/)verification-plan\.md$/.test(file.filename))
71
+ .map((file) => file.filename);
72
+ core.setOutput('sha', pr.head.sha);
73
+ core.setOutput('plans', plans.join('\n'));
74
+ core.setOutput('found', plans.length > 0 ? 'true' : 'false');
75
+ core.info(plans.length > 0
76
+ ? `Verification plans in #${pull_number}: ${plans.join(', ')}`
77
+ : `#${pull_number} carries no verification plan; there is nothing to capture.`);
78
+
79
+ - name: Check out the pull request head
80
+ if: steps.plans.outputs.found == 'true'
81
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
82
+ with:
83
+ ref: ${{ steps.plans.outputs.sha }}
84
+ path: pr
85
+ persist-credentials: false
86
+
87
+ # A plan that only says "no UI surface", or that is prose with no journey, starts nothing and
88
+ # writes nothing: a comment on every backend pull request saying so would be noise.
89
+ - name: Read the plans
90
+ id: inspect
91
+ if: steps.plans.outputs.found == 'true'
92
+ uses: ./.github/actions/capture-evidence
93
+ with:
94
+ mode: inspect
95
+ root: pr
96
+ plans: ${{ steps.plans.outputs.plans }}
97
+ playwright-version: ${{ env.PLAYWRIGHT_VERSION }}
98
+
99
+ - name: Detect the toolchain the app needs
100
+ id: toolchain
101
+ if: steps.inspect.outputs.runnable == 'true'
102
+ working-directory: pr
103
+ run: |
104
+ set -euo pipefail
105
+ global_json="$(find . -maxdepth 3 -name global.json -not -path '*/node_modules/*' | head -n 1)"
106
+ dotnet_project="$(find . -maxdepth 3 \( -name '*.slnx' -o -name '*.sln' -o -name '*.csproj' \) -not -path '*/node_modules/*' | head -n 1)"
107
+ if [ -n "$global_json" ] || [ -n "$dotnet_project" ]; then
108
+ echo "dotnet=true" >> "$GITHUB_OUTPUT"
109
+ else
110
+ echo "dotnet=false" >> "$GITHUB_OUTPUT"
111
+ fi
112
+ echo "global-json=${global_json#./}" >> "$GITHUB_OUTPUT"
113
+
114
+ - name: Set up .NET
115
+ if: steps.toolchain.outputs.dotnet == 'true'
116
+ uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
117
+ with:
118
+ global-json-file: ${{ steps.toolchain.outputs.global-json != '' && format('pr/{0}', steps.toolchain.outputs.global-json) || '' }}
119
+ dotnet-version: ${{ steps.toolchain.outputs.global-json == '' && env.EVIDENCE_DOTNET_VERSION || '' }}
120
+ env:
121
+ # the default /usr/share/dotnet needs root; the tool cache belongs to the runner user
122
+ DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
123
+
124
+ - name: Install the app's dependencies
125
+ if: steps.inspect.outputs.runnable == 'true'
126
+ working-directory: pr
127
+ run: |
128
+ set -euo pipefail
129
+ # Local development reads .env; the example is what a new clone is told to copy.
130
+ if [ ! -f .env ] && [ -f .env.example ]; then cp .env.example .env; fi
131
+ if [ -f pnpm-lock.yaml ]; then
132
+ corepack enable
133
+ corepack prepare --activate
134
+ pnpm install --frozen-lockfile
135
+ elif [ -f package-lock.json ]; then
136
+ npm ci
137
+ elif [ -f package.json ]; then
138
+ npm install --no-audit --no-fund
139
+ fi
140
+
141
+ - name: Start the app
142
+ id: app
143
+ if: steps.inspect.outputs.runnable == 'true'
144
+ working-directory: pr
145
+ env:
146
+ START: ${{ steps.inspect.outputs.start || env.EVIDENCE_START_COMMAND }}
147
+ APP_URL: ${{ steps.inspect.outputs.url }}
148
+ TIMEOUT: ${{ env.EVIDENCE_START_TIMEOUT_SECONDS }}
149
+ run: |
150
+ set -euo pipefail
151
+ log="$RUNNER_TEMP/evidence-app.log"
152
+ # Its own session, so the whole tree it spawns stops with one signal at the end.
153
+ setsid bash -c "$START" > "$log" 2>&1 < /dev/null &
154
+ pid=$!
155
+ {
156
+ echo "pid=$pid"
157
+ echo "start=$START"
158
+ } >> "$GITHUB_OUTPUT"
159
+
160
+ waited=0
161
+ up=false
162
+ # Any HTTP answer counts, a 500 from a dev server still compiling included: the first
163
+ # waypoint's navigation waits for the page itself.
164
+ while [ "$waited" -lt "$TIMEOUT" ]; do
165
+ if curl -s -o /dev/null --max-time 5 "$APP_URL"; then
166
+ up=true
167
+ break
168
+ fi
169
+ if ! kill -0 "$pid" 2>/dev/null; then
170
+ echo "::warning::'$START' exited before the app answered at $APP_URL"
171
+ break
172
+ fi
173
+ sleep 5
174
+ waited=$((waited + 5))
175
+ done
176
+
177
+ {
178
+ echo "up=$up"
179
+ echo "waited=$waited"
180
+ } >> "$GITHUB_OUTPUT"
181
+ if [ "$up" = "true" ]; then
182
+ echo "The app answered at $APP_URL after ${waited}s."
183
+ else
184
+ echo "::warning::the app did not answer at $APP_URL after ${waited}s of '$START'; recording that instead of screenshots"
185
+ echo "::group::Last lines of the app's output"
186
+ tail -n 80 "$log" || true
187
+ echo "::endgroup::"
188
+ fi
189
+
190
+ - name: Replay the plans
191
+ if: steps.app.outputs.up == 'true'
192
+ uses: ./.github/actions/capture-evidence
193
+ env:
194
+ EVIDENCE_SHA: ${{ steps.plans.outputs.sha }}
195
+ EVIDENCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
196
+ with:
197
+ mode: capture
198
+ root: pr
199
+ plans: ${{ steps.plans.outputs.plans }}
200
+ out-dir: ${{ runner.temp }}/evidence
201
+ playwright-version: ${{ env.PLAYWRIGHT_VERSION }}
202
+
203
+ - name: Record that the app never answered
204
+ if: steps.app.outputs.up == 'false'
205
+ uses: ./.github/actions/capture-evidence
206
+ env:
207
+ EVIDENCE_SHA: ${{ steps.plans.outputs.sha }}
208
+ EVIDENCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
209
+ EVIDENCE_APP_URL: ${{ steps.inspect.outputs.url }}
210
+ EVIDENCE_START: ${{ steps.app.outputs.start }}
211
+ EVIDENCE_WAITED_SECONDS: ${{ steps.app.outputs.waited }}
212
+ with:
213
+ mode: unreached
214
+ root: pr
215
+ plans: ${{ steps.plans.outputs.plans }}
216
+ out-dir: ${{ runner.temp }}/evidence
217
+
218
+ - name: Stop the app
219
+ if: always() && steps.app.outputs.pid != ''
220
+ env:
221
+ PID: ${{ steps.app.outputs.pid }}
222
+ run: |
223
+ kill -TERM -- "-$PID" 2>/dev/null || true
224
+ sleep 3
225
+ kill -KILL -- "-$PID" 2>/dev/null || true
226
+
227
+ - name: Hand the evidence to the publish job
228
+ id: done
229
+ if: always() && steps.plans.outputs.found == 'true'
230
+ run: |
231
+ if [ -f "$RUNNER_TEMP/evidence/comment.md" ]; then
232
+ echo "has-evidence=true" >> "$GITHUB_OUTPUT"
233
+ else
234
+ echo "has-evidence=false" >> "$GITHUB_OUTPUT"
235
+ fi
236
+
237
+ - name: Upload the evidence
238
+ if: always() && steps.done.outputs.has-evidence == 'true'
239
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
240
+ with:
241
+ name: evidence-pr-${{ inputs.pr-number }}
242
+ path: ${{ runner.temp }}/evidence
243
+ retention-days: 3
244
+ if-no-files-found: error
245
+
246
+ publish:
247
+ needs: capture
248
+ if: always() && needs.capture.outputs.has-evidence == 'true'
249
+ runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
250
+ timeout-minutes: 10
251
+ permissions:
252
+ contents: write
253
+ pull-requests: write
254
+ issues: write
255
+ steps:
256
+ - name: Download the evidence
257
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
258
+ with:
259
+ name: evidence-pr-${{ inputs.pr-number }}
260
+ path: ${{ runner.temp }}/evidence
261
+
262
+ # An orphan branch rather than the default branch: the images stay out of every build, every
263
+ # diff and every review, and a clone that only wants the code can leave the branch behind.
264
+ # One folder per pull request, so two publishes never touch the same file and a lost push
265
+ # race is a retry, never a conflict.
266
+ - name: Commit the screenshots to the evidence branch
267
+ id: branch
268
+ env:
269
+ TOKEN: ${{ github.token }}
270
+ REPO: ${{ github.repository }}
271
+ SERVER: ${{ github.server_url }}
272
+ BRANCH: ${{ env.EVIDENCE_BRANCH }}
273
+ PR: ${{ inputs.pr-number }}
274
+ run: |
275
+ set -euo pipefail
276
+ src="$RUNNER_TEMP/evidence"
277
+ if ! compgen -G "$src/*.jpg" > /dev/null; then
278
+ echo "No screenshots to keep; the comment records why."
279
+ echo "base=" >> "$GITHUB_OUTPUT"
280
+ exit 0
281
+ fi
282
+
283
+ work="$RUNNER_TEMP/evidence-branch"
284
+ rm -rf "$work"
285
+ mkdir -p "$work"
286
+ cd "$work"
287
+ git init -q
288
+ git config user.name "github-actions[bot]"
289
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
290
+ git config "http.${SERVER}/.extraheader" "AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)"
291
+ git remote add origin "${SERVER}/${REPO}.git"
292
+
293
+ pushed=false
294
+ for attempt in 1 2 3 4 5; do
295
+ if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
296
+ git fetch -q --depth=1 origin "$BRANCH"
297
+ git checkout -q -f -B "$BRANCH" FETCH_HEAD
298
+ else
299
+ git checkout -q -f --orphan "$BRANCH"
300
+ git rm -rqf --cached . > /dev/null 2>&1 || true
301
+ printf '%s\n' "# Evidence" "" \
302
+ "Screenshots the evidence workflow replayed from each merged pull request's verification plan, one folder per pull request." \
303
+ "Written by a workflow, never by hand. Nothing here is built, deployed or merged." > README.md
304
+ git add README.md
305
+ fi
306
+
307
+ rm -rf "pr-$PR"
308
+ mkdir -p "pr-$PR"
309
+ cp "$src"/*.jpg "$src/manifest.json" "pr-$PR/"
310
+ git add -A "pr-$PR"
311
+ if git diff --cached --quiet; then
312
+ pushed=true
313
+ break
314
+ fi
315
+ git commit -q -m "evidence: pull request #$PR"
316
+ if git push -q origin "HEAD:refs/heads/$BRANCH"; then
317
+ pushed=true
318
+ break
319
+ fi
320
+ echo "Another publish moved $BRANCH first (attempt $attempt); retrying on its new tip."
321
+ sleep $((attempt * 3))
322
+ done
323
+ [ "$pushed" = "true" ] || { echo "::error::could not push to $BRANCH after 5 attempts"; exit 1; }
324
+
325
+ sha="$(git rev-parse HEAD)"
326
+ echo "base=${SERVER}/${REPO}/raw/${sha}/pr-${PR}" >> "$GITHUB_OUTPUT"
327
+ echo "Screenshots kept at ${SERVER}/${REPO}/tree/${sha}/pr-${PR}"
328
+
329
+ # GITHUB_TOKEN on purpose: its comment raises no workflow event, so the router does not wake
330
+ # up to read its own record. One comment per pull request, rewritten on a re-run.
331
+ - name: Write the evidence comment
332
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
333
+ env:
334
+ PR_NUMBER: ${{ inputs.pr-number }}
335
+ MARKER: ${{ env.EVIDENCE_MARKER }}
336
+ IMAGE_BASE: ${{ steps.branch.outputs.base }}
337
+ with:
338
+ script: |
339
+ const fs = require('node:fs');
340
+ const path = require('node:path');
341
+ const issue_number = Number(process.env.PR_NUMBER);
342
+ const source = fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'evidence', 'comment.md'), 'utf8');
343
+ const body = source.split('__EVIDENCE_BASE__').join(process.env.IMAGE_BASE);
344
+ const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number, per_page: 100 });
345
+ const existing = comments.find((comment) => comment.user?.type === 'Bot' && (comment.body ?? '').includes(process.env.MARKER));
346
+ if (existing) {
347
+ await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body });
348
+ core.info(`Updated ${existing.html_url}`);
349
+ } else {
350
+ const { data } = await github.rest.issues.createComment({ ...context.repo, issue_number, body });
351
+ core.info(`Created ${data.html_url}`);
352
+ }
@@ -0,0 +1,98 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/workflows/release-tag.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: Release Tag
3
+
4
+ # The second half of the release route. agent-release.md opens a `chore(release): vX.Y.Z` pull
5
+ # request instead of pushing to `main`, so `main` can require pull requests. Once that PR is
6
+ # squash-merged, this tags the merge commit and publishes the GitHub Release with the notes the
7
+ # agent wrote, which the PR body carries. No model is involved.
8
+ #
9
+ # The tag and the Release are created with GITHUB_TOKEN, never the App token, and that is
10
+ # load-bearing. GITHUB_TOKEN writes start no workflows, so a consumer whose deploy triggers on
11
+ # `v*` tags (Numa's app-deploy.yml deploys to production on one) does not ship to production
12
+ # because a release was cut. That is exactly how the release behaved when it pushed the tag
13
+ # itself; moving the bump into a pull request must not change it.
14
+
15
+ on:
16
+ push:
17
+ branches: [main]
18
+
19
+ permissions:
20
+ contents: write
21
+ pull-requests: read
22
+
23
+ concurrency:
24
+ group: release-tag
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ tag:
29
+ name: Tag and publish the release
30
+ if: >-
31
+ startsWith(github.event.head_commit.message, 'chore(release): v')
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 10
34
+ steps:
35
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
+ with:
37
+ persist-credentials: false
38
+ - name: Tag the release commit and publish the GitHub Release
39
+ env:
40
+ GH_TOKEN: ${{ github.token }}
41
+ REPO: ${{ github.repository }}
42
+ SHA: ${{ github.sha }}
43
+ HEAD_MESSAGE: ${{ github.event.head_commit.message }}
44
+ shell: bash
45
+ run: |
46
+ set -euo pipefail
47
+
48
+ # Squash keeps the PR title as the subject and appends ` (#123)`.
49
+ subject="${HEAD_MESSAGE%%$'\n'*}"
50
+ if ! [[ "$subject" =~ ^chore\(release\):\ v([0-9]+\.[0-9]+\.[0-9]+)([[:space:]]|$) ]]; then
51
+ echo "Head commit is not a release commit: $subject"
52
+ exit 0
53
+ fi
54
+ version="${BASH_REMATCH[1]}"
55
+ tag="v${version}"
56
+
57
+ if [ -f package.json ]; then
58
+ manifest=$(node -p "require('./package.json').version")
59
+ if [ "$manifest" != "$version" ]; then
60
+ echo "::error::The commit says $tag but package.json is at $manifest; not tagging."
61
+ exit 1
62
+ fi
63
+ fi
64
+
65
+ # Idempotent: a re-run, or a second push of the same commit, does nothing.
66
+ if gh release view "$tag" --repo "$REPO" >/dev/null 2>&1; then
67
+ echo "Release $tag already exists; nothing to do."
68
+ exit 0
69
+ fi
70
+ if existing=$(gh api "repos/$REPO/git/ref/tags/$tag" --jq '.object.sha' 2>/dev/null); then
71
+ if [ "$existing" != "$SHA" ]; then
72
+ echo "::warning::Tag $tag already exists on $existing, not on $SHA; leaving it alone."
73
+ exit 0
74
+ fi
75
+ fi
76
+
77
+ # The marker tells the release PR apart from any other PR associated with this commit.
78
+ marker="<!-- release:${tag} -->"
79
+ gh api "repos/$REPO/commits/$SHA/pulls" > /tmp/pulls.json
80
+ MARKER="$marker" node -e '
81
+ const pulls = JSON.parse(require("fs").readFileSync("/tmp/pulls.json", "utf8"));
82
+ const pr = pulls.find((p) => p.merged_at && (p.body || "").includes(process.env.MARKER));
83
+ if (!pr) process.exit(3);
84
+ const notes = pr.body.split(/\r?\n/).filter((line) => line.trim() !== process.env.MARKER).join("\n").trim();
85
+ require("fs").writeFileSync("/tmp/release-notes.md", notes + "\n");
86
+ console.log(`Notes from #${pr.number}`);
87
+ ' || status=$?
88
+
89
+ if [ "${status:-0}" -eq 3 ]; then
90
+ echo "::warning::No merged pull request carrying $marker is associated with $SHA; publishing generated notes instead."
91
+ gh release create "$tag" --repo "$REPO" --target "$SHA" --title "$tag" --generate-notes
92
+ elif [ "${status:-0}" -ne 0 ]; then
93
+ echo "::error::Could not read the release notes from the pull request."
94
+ exit 1
95
+ else
96
+ gh release create "$tag" --repo "$REPO" --target "$SHA" --title "$tag" --notes-file /tmp/release-notes.md
97
+ fi
98
+ echo "Tagged $SHA as $tag and published the GitHub Release."
@@ -139,12 +139,11 @@ pre-agent-steps:
139
139
  npm install -g "@fission-ai/openspec@1.8.0"
140
140
  openspec --version
141
141
 
142
- # NOTE: playwright-cli and SQL Server startup steps are not part of the shared CI
143
- # baseline. The agent path captures no visual evidence: it cannot reach a browser
144
- # from inside the awf sandbox, and a screenshot with no baseline to compare against
145
- # gated nothing. Run /ops-evidence locally when a change warrants screenshots. If
146
- # you need playwright-cli or SQL Server inside the agent sandbox for other reasons,
147
- # add them as consumer-specific steps after the shared baseline.
142
+ # NOTE: no browser and no SQL Server in the shared CI baseline. An agent cannot reach a
143
+ # browser from inside the awf sandbox, so it only writes the verification plan
144
+ # (/repo-verify); evidence.yml replays that plan outside the sandbox once the pull request
145
+ # merges and archives the screenshots on it. If you need SQL Server inside the agent sandbox
146
+ # for other reasons, add it as a consumer-specific step after the shared baseline.
148
147
 
149
148
  - name: Setup .NET
150
149
  # Ephemeral VMs carry no SDK; the old host had it baked in, and GitHub-hosted images made
@@ -67,8 +67,8 @@ run-name: >-
67
67
  && inputs.operation == 'merge-gate'
68
68
  && format('Agent: Merge Gate - PR #{0}', inputs.pr-number)
69
69
  || github.event_name == 'workflow_dispatch'
70
- && inputs.operation == 'visual-verify'
71
- && format('Agent: Visual Verify - PR #{0}', inputs.pr-number)
70
+ && inputs.operation == 'evidence'
71
+ && format('Agentics: Evidence - PR #{0}', inputs.pr-number)
72
72
  || github.event_name == 'workflow_dispatch'
73
73
  && inputs.operation == 'batch'
74
74
  && format('Working: Batch - #{0}', inputs.issue-number)
@@ -144,6 +144,11 @@ env:
144
144
  # merge-gate worker carried its own MAX_ATTEMPTS; the route matrix now asserts the two
145
145
  # agree, because a belt that gives up later than the worker says burns whole runs.
146
146
  MAX_GATE_ATTEMPTS: "6"
147
+ # Whether a merged pull request's verification plan is replayed and its screenshots archived on
148
+ # the pull request (evidence.yml). It needs nothing configured: the app starts with the root
149
+ # `pnpm run dev`, and a pull request with no plan costs one API call. Set it to "false" for a
150
+ # repository whose app cannot start on a runner; operation=evidence still works by hand.
151
+ EVIDENCE_ENABLED: "true"
147
152
 
148
153
  on:
149
154
  issues:
@@ -158,7 +163,8 @@ on:
158
163
  pull_request_target:
159
164
  # opened / reopened / synchronize feed conflict detection (detect-pr-conflicts) and
160
165
  # bot-approve. closed feeds detect-pr-conflicts: when a PR merges, the target branch
161
- # moved and previously-mergeable sibling bot PRs may now conflict.
166
+ # moved and previously-mergeable sibling bot PRs may now conflict. A merge also routes to
167
+ # evidence, which replays the pull request's verification plan.
162
168
  types: [opened, synchronize, reopened, labeled, closed]
163
169
 
164
170
  # CI completion on a pull request. The classifier admits only conclusion == failure with an
@@ -199,7 +205,7 @@ on:
199
205
  - triage
200
206
  - apply-review
201
207
  - merge-gate
202
- - visual-verify
208
+ - evidence
203
209
  - audit
204
210
  - audit-close
205
211
  - cleanup-artifacts
@@ -212,7 +218,7 @@ on:
212
218
  required: false
213
219
  type: string
214
220
  pr-number:
215
- description: "Pull request number (required for apply-review / merge-gate)"
221
+ description: "Pull request number (required for apply-review / merge-gate / evidence)"
216
222
  required: false
217
223
  type: string
218
224
  mode:
@@ -623,18 +629,24 @@ jobs:
623
629
  BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
624
630
  AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
625
631
 
626
- call-visual-verify:
632
+ # Screenshots of a merged pull request, replayed from the verification plan its implementation
633
+ # wrote and archived on the pull request. Plain YAML with no model, so no secret is passed and the
634
+ # grant is exactly what the publish job writes: the evidence branch and one comment. Its capture
635
+ # job runs the pull request's code with read-only access; only publish holds these writes, and it
636
+ # runs no repository code.
637
+ call-evidence:
627
638
  needs: classify
628
- if: needs.classify.outputs.route == 'visual-verify'
629
- uses: ./.github/workflows/agent-visual-verify.lock.yml
630
- permissions: write-all
639
+ if: needs.classify.outputs.route == 'evidence'
640
+ uses: ./.github/workflows/evidence.yml
641
+ concurrency:
642
+ group: evidence-${{ needs.classify.outputs.pr-number }}
643
+ cancel-in-progress: false
644
+ permissions:
645
+ contents: write
646
+ pull-requests: write
647
+ issues: write
631
648
  with:
632
649
  pr-number: ${{ needs.classify.outputs.pr-number }}
633
- linked-issue: ${{ needs.classify.outputs.linked-issue }}
634
- secrets:
635
- OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
636
- CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
637
- COPILOT_GITHUB_TOKEN: ${{ secrets.FORGE_API_KEY }}
638
650
 
639
651
  call-audit:
640
652
  needs: classify
@@ -663,6 +675,9 @@ jobs:
663
675
  version-bump: ${{ github.event.inputs.version-bump || 'auto' }}
664
676
  secrets:
665
677
  OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
678
+ # The release PR is pushed and opened with the App token so its CI runs; see conclude.
679
+ BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
680
+ BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
666
681
 
667
682
  bot-approve:
668
683
  needs: classify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.0.14",
3
+ "version": "1.1.1",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -1,93 +0,0 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/actions/attach-screenshots/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
- name: Attach screenshots
3
- description: Post screenshots captured by agent-browser to the linked issue as a single comment.
4
- inputs:
5
- token:
6
- description: GitHub token with issues:write access.
7
- required: true
8
- issue-number:
9
- description: Issue number to comment on.
10
- required: true
11
- pr-number:
12
- description: Pull request number being verified.
13
- required: true
14
- screenshots-dir:
15
- description: Directory containing screenshots and plan.json.
16
- required: true
17
- screenshot-count:
18
- description: Number of screenshots captured (0 if none).
19
- required: false
20
- default: '0'
21
- app-started:
22
- description: Whether the app started successfully.
23
- required: false
24
- default: 'false'
25
- enabled:
26
- description: Whether visual verification was enabled.
27
- required: false
28
- default: 'true'
29
- runs:
30
- using: composite
31
- steps:
32
- - name: Attach screenshots to issue
33
- shell: bash
34
- env:
35
- GH_TOKEN: ${{ inputs.token }}
36
- REPO: ${{ github.repository }}
37
- ISSUE: ${{ inputs.issue-number }}
38
- PR: ${{ inputs.pr-number }}
39
- SCREENSHOTS_DIR: ${{ inputs.screenshots-dir }}
40
- SCREENSHOT_COUNT: ${{ inputs.screenshot-count }}
41
- APP_STARTED: ${{ inputs.app-started }}
42
- ENABLED: ${{ inputs.enabled }}
43
- run: |
44
- set -euo pipefail
45
-
46
- if [ "$ENABLED" != "true" ]; then
47
- echo "Visual verification disabled; nothing to attach."
48
- exit 0
49
- fi
50
-
51
- if [ -f "$SCREENSHOTS_DIR/plan.json" ] && [ "$APP_STARTED" = "true" ] && [ "$SCREENSHOT_COUNT" -gt 0 ]; then
52
- # Build the comment body from the plan's descriptions.
53
- body=$(jq -r --arg pr "$PR" '
54
- "<!-- agent-visual-verify -->\n## Visual verification for PR #" + $pr + "\n",
55
- (.waypoints | to_entries[] |
56
- "**\(.key + 1). " + (.value.description // .value.expect // "waypoint") + "**\n" +
57
- "Selector: `" + (.value.selector // "n/a") + "`\n"
58
- )
59
- ' "$SCREENSHOTS_DIR/plan.json")
60
-
61
- # Collect all screenshot files.
62
- attaches=()
63
- for png in "$SCREENSHOTS_DIR"/*.png; do
64
- [ -f "$png" ] || continue
65
- attaches+=("$png")
66
- done
67
-
68
- if [ ${#attaches[@]} -gt 0 ]; then
69
- # --body-file with --attach is not supported by every gh version; --body with a
70
- # here-string is, and one comment per screenshot loses the narrative. Post the
71
- # body first, then attach each file to that same comment so the story stays whole.
72
- comment_url=$(gh issue comment "$ISSUE" --repo "$REPO" --body "$body" --json url --jq '.url')
73
- for png in "${attaches[@]}"; do
74
- gh issue comment "$ISSUE" --repo "$REPO" --attach "$png" --body "" 2>/dev/null || true
75
- done
76
- echo "Attached ${#attaches[@]} screenshots to issue #$ISSUE ($comment_url)"
77
- else
78
- gh issue comment "$ISSUE" --repo "$REPO" --body "$body"
79
- echo "No screenshots found; posted plan summary only"
80
- fi
81
- else
82
- # App didn't start or no plan was produced. Post a warning.
83
- reason="visual verification could not produce screenshots"
84
- if [ "$APP_STARTED" != "true" ]; then
85
- reason="$reason (app did not start)"
86
- elif [ ! -f "$SCREENSHOTS_DIR/plan.json" ]; then
87
- reason="$reason (no verification plan was written)"
88
- fi
89
-
90
- warning_body=$(printf '<!-- agent-visual-verify -->## Visual verification for PR #%s\n\n%s' "$PR" "$reason. The merge proceeds without visual evidence.")
91
- gh issue comment "$ISSUE" --repo "$REPO" --body "$warning_body"
92
- echo "Posted warning: $reason"
93
- fi