@plainconceptsplatform/workflows 1.12.0 → 1.13.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.
@@ -100,7 +100,7 @@ flowchart LR
100
100
  gate -.->|"CI failed or conflict"| fix("Fix, push, CI again") --> ci
101
101
  gate -.->|".github, secrets<br/>or 10000+ lines"| human(("Human review"))
102
102
  review("Someone reviews the PR") --> applyReview("apply-review") --> ci
103
- audit("audit, weekly") -->|"files one issue"| refine
103
+ audit("audit, weekly") -->|"files stories"| refine
104
104
 
105
105
  classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
106
106
  classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
@@ -114,9 +114,7 @@ flowchart LR
114
114
  class merged success
115
115
  ```
116
116
 
117
- Audit creates work rather than consuming it, and files into `refine` rather than straight to
118
- `implement`, so a report of several unrelated findings becomes one properly sized issue per finding
119
- instead of one pull request that has to fix them all.
117
+ Audit creates work rather than consuming it, and files into `refine` rather than straight to `implement`: one story issue per top finding, each under refine's size limit, plus a tracking report refine never sees, instead of one pull request that has to fix them all.
120
118
 
121
119
  ### The merge train
122
120
 
@@ -489,7 +487,7 @@ before anything changes. Applying them one at a time produces contradictory comm
489
487
 
490
488
  ## agent-audit.md
491
489
 
492
- Creates work rather than consuming it. Read-only: it files an issue and changes nothing.
490
+ Creates work rather than consuming it. Read-only: it files a tracking report and one story issue per top finding, and changes nothing else.
493
491
 
494
492
  ```mermaid
495
493
  flowchart TD
@@ -506,8 +504,9 @@ flowchart TD
506
504
  auDedupe{"Already tracked, or<br/>previously rejected?"}
507
505
  auDedupe -.->|"all of them"| auQuiet
508
506
  auDedupe -->|"some are new"| auFile
509
- auFile("One issue: every finding,<br/>top 3 refined into stories") --> auReport
510
- auReport(("Filed<br/>labelled refine, so it gets<br/>sized and split per finding"))
507
+ auFile("Report of every finding,<br/>plus one story per top 3") --> auMeasure
508
+ auMeasure("conclude (rung 6)<br/>each body measured with<br/>refine's rule, trimmed to fit") --> auReport
509
+ auReport(("Filed<br/>report: audit, not refine<br/>stories: refine, sub-issues"))
511
510
  auQuiet(("Nothing to file<br/>the right outcome on a<br/>clean codebase"))
512
511
  auIdle(("Idle<br/>backlog already full"))
513
512
 
@@ -517,12 +516,14 @@ flowchart TD
517
516
  classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
518
517
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
519
518
  class auStart start
520
- class auFacts,auRun,auScore,auFile action
519
+ class auFacts,auRun,auScore,auFile,auMeasure action
521
520
  class auBack,auFilter,auDedupe decision
522
521
  class auIdle idle
523
522
  class auReport,auQuiet success
524
523
  ```
525
524
 
525
+ The report never goes to refine: refine's size gate refused every audit filed as one issue of every finding and three stories, from 2026-09-21 until 1.12.1. Each story is filed on its own, labelled `refine`, linked under the report and listed in it, so `audit-close` closes the report once its stories are closed. A story already tracked by an open issue is listed against that issue instead of filed again.
526
+
526
527
  Memory matters here: open issues only cover what is still open, so a finding reported weeks ago and
527
528
  consciously not acted on would come back every single run. The audit records its dispositions and
528
529
  reads them next time.
@@ -549,8 +550,9 @@ flowchart TD
549
550
  daDedupe{"New finding, proven<br/>on both sides?"}
550
551
  daDedupe -.->|"none left"| daQuiet
551
552
  daDedupe -->|"some"| daFile
552
- daFile("One issue with<br/>evidence per finding") --> daReport
553
- daReport(("Filed<br/>labelled docs-audit and refine,<br/>sized and split per finding"))
553
+ daFile("Report of every finding,<br/>plus one story per top 3") --> daMeasure
554
+ daMeasure("conclude (rung 6)<br/>each body measured with<br/>refine's rule, trimmed to fit") --> daReport
555
+ daReport(("Filed<br/>report: docs-audit, not refine<br/>stories: refine, sub-issues"))
554
556
  daQuiet(("Nothing to file<br/>the documentation is current"))
555
557
  daIdle(("Idle<br/>no agent, no spend"))
556
558
 
@@ -560,7 +562,7 @@ flowchart TD
560
562
  classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
561
563
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
562
564
  class daStart start
563
- class daFacts,daRun,daFile action
565
+ class daFacts,daRun,daFile,daMeasure action
564
566
  class daBack,daMoved,daDedupe decision
565
567
  class daIdle idle
566
568
  class daReport,daQuiet success
@@ -569,6 +571,8 @@ flowchart TD
569
571
  Reports join the audit chain: `audit-close` closes one when its children are closed, and marks it
570
572
  `stale-audit` when nobody acted on it, which frees the slot.
571
573
 
574
+ It files the way the audit does, through the same `file-audit-issues` step: the report never goes to refine, which refused a single report of every finding once it grew past its size gate. Each top story is filed on its own, labelled `refine`, linked under the report and listed in it.
575
+
572
576
  ---
573
577
 
574
578
  ## agent-release.md
@@ -652,7 +656,8 @@ flowchart TB
652
656
  hkBranch -->|yes| hkDelete("Delete the branch")
653
657
  hkBranch -.->|"open PR, human PR,<br/>or the default branch"| hkDigest
654
658
  hkDelete --> hkDigest
655
- hkDigest("Rewrite one issue:<br/>Needs a human") --> hkEnd
659
+ hkDigest("Rewrite one issue:<br/>Needs a human") --> hkDependabot
660
+ hkDependabot("Squash-merge Dependabot PRs<br/>whose checks are all green") --> hkEnd
656
661
  hkEnd(("One place to look<br/>instead of four repos"))
657
662
 
658
663
  classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
@@ -660,7 +665,7 @@ flowchart TB
660
665
  classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
661
666
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
662
667
  class hkCron start
663
- class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest,hkStackClose action
668
+ class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest,hkStackClose,hkDependabot action
664
669
  class hkPark,hkWhy,hkBudget,hkBranch,hkStack,hkStackWait decision
665
670
  class hkEnd success
666
671
  ```
@@ -681,6 +686,8 @@ held for days after its announcement, a root closed by accident with children op
681
686
  root pull request each get one line in the digest. No labels move, so nothing the router listens
682
687
  for is raised by the sweep itself.
683
688
 
689
+ The last step merges Dependabot's updates through the `merge-dependabot` action: only pull requests Dependabot opened with every commit verified and its own, squash-merged once every check is green, a stale one asked once to rebase, a failing one listed in the job summary. It reads nothing of the pull request but the API, and `DEPENDABOT_MERGE` sets its scope. See `docs/dependabot-merge.md` in the package.
690
+
684
691
  ---
685
692
 
686
693
  ## agentics-app-errors
@@ -963,6 +963,9 @@ jobs:
963
963
  # If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
964
964
  # `actions: write`. A called workflow may not request a scope its caller withheld, and the
965
965
  # whole run fails to start with zero jobs and no annotation when it does.
966
+ #
967
+ # Not for Dependabot: its runs get a read-only token, so the dispatch would fail its pull request,
968
+ # and the router's maintenance run merges Dependabot updates instead of the train.
966
969
  dispatch-merge-gate:
967
970
  name: Dispatch merge gate
968
971
  needs: [api, mutation-api, web, mutation-web, secret-scan, deps-and-iac-scan, sast-scan, sbom, engineering-excellence, platform-conformance, full-ci]
@@ -972,6 +975,7 @@ jobs:
972
975
  github.event_name == 'pull_request' &&
973
976
  github.event.pull_request.head.repo.full_name == github.repository &&
974
977
  github.event.pull_request.draft == false &&
978
+ github.event.pull_request.user.login != 'dependabot[bot]' &&
975
979
  (github.event.pull_request.user.login == 'app/github-actions' || endsWith(github.event.pull_request.user.login, '[bot]'))
976
980
  runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
977
981
  timeout-minutes: 5
@@ -239,6 +239,9 @@ jobs:
239
239
  # If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
240
240
  # `actions: write`. A called workflow may not request a scope its caller withheld, and the
241
241
  # whole run fails to start with zero jobs and no annotation when it does.
242
+ #
243
+ # Not for Dependabot: its runs get a read-only token, so the dispatch would fail its pull request,
244
+ # and the router's maintenance run merges Dependabot updates instead of the train.
242
245
  dispatch-merge-train:
243
246
  name: Dispatch merge train
244
247
  needs: [lint, web, mutation-web, e2e, desktop, mobile]
@@ -246,7 +249,8 @@ jobs:
246
249
  always() &&
247
250
  github.event_name == 'pull_request' &&
248
251
  github.event.pull_request.head.repo.full_name == github.repository &&
249
- github.event.pull_request.draft == false
252
+ github.event.pull_request.draft == false &&
253
+ github.event.pull_request.user.login != 'dependabot[bot]'
250
254
  runs-on: agents-arc
251
255
  timeout-minutes: 5
252
256
  permissions:
@@ -0,0 +1,80 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/templates/dependabot-merge/.github/workflows/dependabot-merge.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: "Dependabot: Merge"
3
+
4
+ # Every six hours, squash-merges the open Dependabot pull requests whose checks are all green, for a
5
+ # repository that does not run the work router (the router does the same in its maintenance run).
6
+ # Plain YAML, no model, and the pull request's code is never checked out: the checkout below is the
7
+ # default branch, for the action. Only pull requests Dependabot opened, with every commit
8
+ # Dependabot's, are touched. See docs/dependabot-merge.md in agentic-workflows.
9
+ #
10
+ # With the Platform App secrets (BOT_APP_ID / BOT_PRIVATE_KEY) the merge runs as the App, and the
11
+ # merge commit starts the default branch's push workflows. Without them it runs as GITHUB_TOKEN,
12
+ # and GitHub starts no workflow from a merge made with that token: nothing deploys on push until
13
+ # the next commit that is not a Dependabot merge.
14
+ #
15
+ # To turn it off: set DEPENDABOT_MERGE to "off" below, or the repository variable DEPENDABOT_MERGE
16
+ # to "off", or AGENTIC_WORKFLOWS_DISABLED to anything, or delete this file.
17
+ on:
18
+ schedule:
19
+ - cron: "41 */6 * * *"
20
+ workflow_dispatch:
21
+ inputs:
22
+ dry-run:
23
+ description: "Decide and report, merge nothing"
24
+ required: false
25
+ type: boolean
26
+ default: false
27
+
28
+ permissions:
29
+ contents: read
30
+
31
+ concurrency:
32
+ group: dependabot-merge
33
+ cancel-in-progress: false
34
+
35
+ env:
36
+ # "all", "minor" (minor and patch), "patch", or "off". CI is the gate.
37
+ DEPENDABOT_MERGE: "all"
38
+ # "true" also merges in a repository with no checks at all, once the head is an hour old.
39
+ DEPENDABOT_MERGE_WITHOUT_CHECKS: "true"
40
+
41
+ jobs:
42
+ merge:
43
+ name: Merge Dependabot pull requests
44
+ if: "!github.event.repository.fork && vars.AGENTIC_WORKFLOWS_DISABLED == ''"
45
+ runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}
46
+ timeout-minutes: 15
47
+ permissions:
48
+ # Merging, deleting the merged branch, approving where a ruleset asks for it, and commenting
49
+ # `@dependabot rebase`. All of it through the API. checks and statuses: the reads run on
50
+ # this token, since the App installation may hold no Checks permission.
51
+ contents: write
52
+ pull-requests: write
53
+ issues: write
54
+ checks: read
55
+ statuses: read
56
+ env:
57
+ HAS_APP: ${{ secrets.BOT_APP_ID != '' && secrets.BOT_PRIVATE_KEY != '' }}
58
+ steps:
59
+ - name: Checkout the action
60
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
61
+ with:
62
+ persist-credentials: false
63
+ sparse-checkout: .github/actions/merge-dependabot
64
+
65
+ - name: Generate App token
66
+ id: app-token
67
+ if: env.HAS_APP == 'true'
68
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
69
+ with:
70
+ app-id: ${{ secrets.BOT_APP_ID }}
71
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
72
+
73
+ - uses: ./.github/actions/merge-dependabot
74
+ with:
75
+ token: ${{ steps.app-token.outputs.token || github.token }}
76
+ read-token: ${{ github.token }}
77
+ scope: ${{ vars.DEPENDABOT_MERGE || env.DEPENDABOT_MERGE }}
78
+ merge-without-checks: ${{ env.DEPENDABOT_MERGE_WITHOUT_CHECKS }}
79
+ post-merge-event: ""
80
+ dry-run: ${{ github.event.inputs.dry-run || 'false' }}
@@ -19,15 +19,23 @@ env:
19
19
  # One line: gh-aw joins a multi-line env value onto a single line when it compiles the lock.
20
20
  AUDIT_FOCUS: "Architectural layer violations and dependencies pointing the wrong way. Missing or misleading tests around behaviour that already shipped. Security gaps: unvalidated input, missing authorization, secrets in code. Performance anti-patterns, N+1 queries in particular. Documentation that no longer matches the code it describes."
21
21
  AUDIT_MARKER: "<!-- agent-audit -->"
22
+ # The largest issue the audit files, measured with refine's own rule (issue-size.cjs). Three
23
+ # quarters of agent-refine.md's REFINE_MAX_BODY_CHARS and REFINE_MAX_WORK_UNITS, so a story a
24
+ # person edits a little still refines. A body over these is trimmed before it is filed.
25
+ AUDIT_MAX_BODY_CHARS: "6000"
26
+ AUDIT_MAX_WORK_UNITS: "18"
27
+ AUDIT_MAX_STORIES: "3"
22
28
  GIT_AUTHOR_NAME: "github-actions[bot]"
23
29
  GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
24
30
  GIT_COMMITTER_NAME: "github-actions[bot]"
25
31
  GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
26
32
  description: |
27
- Read-only repository audit. Finds 5-7 problems, scores each 1-10, files a single issue
28
- with all findings listed and the top 3 refined as actionable user stories. The issue is
29
- labelled `audit` + `bug` + `refine`, so Refine sizes it and splits a multi-defect report
30
- into one estimated issue per finding rather than one pull request that has to fix them all. Replaces .loops/recipes/guardrails-audit-loop.yaml.
33
+ Read-only repository audit. Finds 5-7 problems, scores each 1-10, and files a compact
34
+ tracking report of every finding, labelled `audit` + `bug`, plus one story issue for each of
35
+ the top 3, labelled `bug` + `refine` and linked to the report as sub-issues. Refine refines
36
+ each story on its own; the report never goes to Refine. Every body is measured with refine's
37
+ size rule before it is filed and trimmed to fit, because one issue holding every finding and
38
+ three stories was over refine's limit every time and was refused. Replaces .loops/recipes/guardrails-audit-loop.yaml.
31
39
 
32
40
  Router-only worker: triggered exclusively via workflow_call from work-router.yml.
33
41
  Contract input: trigger-kind(scheduled|manual).
@@ -54,7 +62,8 @@ on:
54
62
  type: string
55
63
  default: manual
56
64
 
57
- # Rung 1. Do not pile reports on top of unactioned reports.
65
+ # Rung 1. Do not pile reports on top of unactioned reports. Only the report carries `audit`;
66
+ # its stories do not, or one audit would fill the three slots by itself.
58
67
  #
59
68
  # `-label:stale-audit` is what keeps this backpressure from becoming a stop. Three reports
60
69
  # nobody ever actioned used to disable the weekly audit for good: the query counted them
@@ -115,19 +124,27 @@ jobs:
115
124
  with:
116
125
  client-id: ${{ secrets.BOT_APP_ID }}
117
126
  private-key: ${{ secrets.BOT_PRIVATE_KEY }}
118
- # No apply-agent-output here. The safe_outputs job already created the issue and
119
- # exposes its number; calling the action with create-issues would file a second copy
120
- # of the same report. It was only needed while staged: true suppressed the native
121
- # write, and staged is gone.
122
- - name: Apply audit labels to created issues
123
- if: needs.safe_outputs.outputs.created_issue_number != ''
124
- uses: ./.github/actions/add-issue-labels
127
+ # Staged, so this is the only write: the report, then each story with its labels and a
128
+ # link back, then the report's list of stories. Each body passes refine's size rule first.
129
+ - name: Download agent output
130
+ id: output
131
+ uses: ./.github/actions/download-agent-output
125
132
  with:
133
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
134
+ - name: File the report and its stories
135
+ uses: ./.github/actions/file-audit-issues
136
+ with:
137
+ output-file: ${{ steps.output.outputs.output-file }}
126
138
  token: ${{ steps.app-token.outputs.token }}
127
- issue-number: ${{ needs.safe_outputs.outputs.created_issue_number }}
128
- labels: |
139
+ max-body-chars: ${{ env.AUDIT_MAX_BODY_CHARS }}
140
+ max-work-units: ${{ env.AUDIT_MAX_WORK_UNITS }}
141
+ max-stories: ${{ env.AUDIT_MAX_STORIES }}
142
+ run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
143
+ report-labels: |-
129
144
  audit
130
145
  bug
146
+ story-labels: |-
147
+ bug
131
148
  refine
132
149
 
133
150
  # An audit that produced nothing reported success. The whole run -- a full agent, its tokens,
@@ -138,7 +155,7 @@ jobs:
138
155
  #
139
156
  # This fires when the agent succeeded and the safe-outputs handler processed nothing at all.
140
157
  #
141
- # Step 6 of the prompt gives a clean codebase its own outcome -- call `noop` and stop -- and
158
+ # Step 7 of the prompt gives a clean codebase its own outcome -- call `noop` and stop -- and
142
159
  # that outcome must not be reported as a failure. `noop` is a registered safe output here
143
160
  # (`noop: max 1`), so the handler receives it as a message and the processed count is not zero,
144
161
  # which keeps a deliberately empty audit out of this branch. That is read from the handler
@@ -171,8 +188,11 @@ safe-outputs:
171
188
  # under noise nobody closes.
172
189
  report-failure-as-issue: false
173
190
  threat-detection: false
191
+ # The report and the stories are filed by `conclude`, after they are measured.
192
+ staged: true
193
+ # The report and up to three stories.
174
194
  create-issue:
175
- max: 1
195
+ max: 4
176
196
 
177
197
  # The fleet is two machines, so this clock is also how long a stuck run can hold half of it.
178
198
  # 240 went on to every worker at once when the provider was slow, which fixed the deaths and
@@ -227,21 +247,18 @@ timeout-minutes: 90
227
247
  again. What this prevents is re-filing something the team looked at and chose to live
228
248
  with, which is the noise that makes an audit backlog get ignored.
229
249
 
230
- 5. Call `create_issue` **once** with title "Audit: <date>". Do NOT specify labels , the
231
- conclude job will apply them. The body MUST have two sections:
250
+ 5. File the report: call `create_issue` with title "Audit: <YYYY-MM-DD>" and nothing else in the title. Do NOT specify labels; the conclude job applies them. The report is a compact tracking list, not a story: Refine never reads it. One entry per finding, highest score first, each exactly this shape and no longer than about 600 characters:
232
251
 
233
- **Section 1 , All findings:** A numbered list of every finding (5-7) with its score,
234
- file path, and a one-line description. Order by score descending.
252
+ ```
253
+ ### <n>. <one-line problem> (score <s>)
254
+ **Where:** `path/to/file.ext:line`, `other/file.ext:line`
255
+ **Problem:** One or two sentences: what happens and its impact.
256
+ **Fix:** One or two sentences: the change that resolves it.
257
+ ```
235
258
 
236
- **Section 2 , Top 3 to implement:** Call skill("pc-plan-story") and refine the top 3
237
- findings by score into user stories; that skill owns their shape. Name the likely files to
238
- change. Mark this section clearly with a heading like `## Top 3 , To Implement`.
259
+ No bullet lists, no Gherkin, no user stories, no reproduction walkthroughs. Do not write any issue number (`#123`) in the report: the workflow adds the links to the stories, and every number in it is read as work the report waits on.
239
260
 
240
- The issue you file goes to Refine, not straight to implementation. Refine sizes it and,
241
- because a report of several unrelated defects across different files is exactly the shape
242
- that does not land as one pull request, splits it into one properly estimated issue per
243
- finding. Write each finding so it survives that split: self-contained, naming its own
244
- files and its own acceptance criteria, never "as above" or "same as finding 2".
261
+ 6. File the stories: call skill("pc-plan-story") and write the top ${{ env.AUDIT_MAX_STORIES }} findings by score as user stories; that skill owns their shape. Call `create_issue` once per story, after the report, titled with the story's own title (never "Audit: ..."), no labels. Refine refines each story on its own, so each must stand alone: it names its own files and its own acceptance criteria, never "as above" or "see finding 2", and never mentions the other stories or the report. Keep each story well under ${{ env.AUDIT_MAX_BODY_CHARS }} characters and ${{ env.AUDIT_MAX_WORK_UNITS }} bullet lines: the workflow measures every issue against that limit before filing and cuts anything over it from the end.
245
262
 
246
- 6. If nothing met the bar, call `noop` and stop. Filing nothing is the right outcome when
263
+ 7. If nothing met the bar, call `noop` and stop. Filing nothing is the right outcome when
247
264
  the codebase is clean.
@@ -19,15 +19,23 @@ env:
19
19
  # No commit in this many days means nothing can have drifted since the last run, so a scheduled
20
20
  # run stops before the model starts. 17 is the longest gap between runs on days 1 and 15.
21
21
  DOCS_AUDIT_QUIET_DAYS: "17"
22
+ # The largest issue the docs audit files, measured with refine's own rule (issue-size.cjs), as
23
+ # agent-audit.md does: three quarters of refine's limits. A body over these is trimmed first.
24
+ DOCS_AUDIT_MAX_BODY_CHARS: "6000"
25
+ DOCS_AUDIT_MAX_WORK_UNITS: "18"
26
+ DOCS_AUDIT_MAX_STORIES: "3"
22
27
  GIT_AUTHOR_NAME: "github-actions[bot]"
23
28
  GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
24
29
  GIT_COMMITTER_NAME: "github-actions[bot]"
25
30
  GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
26
31
  description: |
27
32
  Read-only documentation audit. Checks that the repository's documentation still matches its
28
- code and the Platform documentation standard. When it does not, files a single issue of
29
- findings with evidence from both sides, labelled `docs-audit` + `refine`, so Refine sizes it
30
- and splits it into one issue per finding. When the documentation is current, files nothing.
33
+ code and the Platform documentation standard. When it does not, files a compact tracking report
34
+ of every finding, labelled `docs-audit` and never `refine`, plus one story issue for each of the
35
+ top 3 findings, labelled `documentation` + `refine` and linked to the report as sub-issues.
36
+ Every body is measured with refine's size rule before it is filed and trimmed to fit, because a
37
+ single report of every finding grows past refine's limit and is refused. When the documentation
38
+ is current, files nothing.
31
39
 
32
40
  Router-only worker: triggered exclusively via workflow_call from work-router.yml.
33
41
  Contract input: trigger-kind(scheduled|manual).
@@ -173,16 +181,27 @@ jobs:
173
181
  with:
174
182
  client-id: ${{ secrets.BOT_APP_ID }}
175
183
  private-key: ${{ secrets.BOT_PRIVATE_KEY }}
176
- # The safe_outputs job already created the issue. Only the labels are left, and they are
177
- # applied with the App token so that `refine` reaches the router as a trusted event.
178
- - name: Apply labels to the report
179
- if: needs.safe_outputs.outputs.created_issue_number != ''
180
- uses: ./.github/actions/add-issue-labels
184
+ # Staged, so this is the only write, as in agent-audit.md: the report, each story with its
185
+ # labels and a link back, then the report's list. The App token lets `refine` reach the router.
186
+ - name: Download agent output
187
+ id: output
188
+ uses: ./.github/actions/download-agent-output
181
189
  with:
190
+ artifact-name: ${{ needs.activation.outputs.artifact_prefix }}agent
191
+ - name: File the report and its stories
192
+ uses: ./.github/actions/file-audit-issues
193
+ with:
194
+ output-file: ${{ steps.output.outputs.output-file }}
182
195
  token: ${{ steps.app-token.outputs.token }}
183
- issue-number: ${{ needs.safe_outputs.outputs.created_issue_number }}
184
- labels: |
196
+ max-body-chars: ${{ env.DOCS_AUDIT_MAX_BODY_CHARS }}
197
+ max-work-units: ${{ env.DOCS_AUDIT_MAX_WORK_UNITS }}
198
+ max-stories: ${{ env.DOCS_AUDIT_MAX_STORIES }}
199
+ run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
200
+ report-title: '^docs audit:'
201
+ report-labels: |-
185
202
  docs-audit
203
+ story-labels: |-
204
+ documentation
186
205
  refine
187
206
 
188
207
  # An agent that ended without a report or a noop would otherwise be a green run that audited
@@ -208,8 +227,11 @@ safe-outputs:
208
227
  # A failed run is already a red run. An issue per failure buries the real backlog.
209
228
  report-failure-as-issue: false
210
229
  threat-detection: false
230
+ # The report and the stories are filed by `conclude`, after they are measured.
231
+ staged: true
232
+ # The report and up to three stories.
211
233
  create-issue:
212
- max: 1
234
+ max: 4
213
235
 
214
236
  # Reads documents and the code they describe, and writes one issue. Unobserved so far: audit, the
215
237
  # nearest worker, takes about 45 minutes over a whole repository, and this one reads less.
@@ -253,14 +275,18 @@ timeout-minutes: 60
253
275
  7. If no finding is left, call `noop` and stop. Filing nothing is the right outcome when the
254
276
  documentation is current.
255
277
 
256
- 8. Call `create_issue` **once** with title "Docs audit: <date>". Do NOT specify labels, the
257
- conclude job applies them. List every finding, the most misleading first, each with:
258
- - the document's `file:line` and the stale statement;
259
- - the evidence from the code, with its `file:line` or path;
260
- - what the document should say, or the section that has to be added.
278
+ 8. File the report: call `create_issue` with title "Docs audit: <YYYY-MM-DD>" and nothing else in the title. Do NOT specify labels; the conclude job applies them. The report is a compact tracking list, not a story: Refine never reads it. One entry per finding, the most misleading first, each exactly this shape and no longer than about 600 characters:
279
+
280
+ ```
281
+ ### <n>. <one-line stale statement>
282
+ **Document:** `path/to/doc.md:line`, what it says
283
+ **Evidence:** `path/to/code.ext:line` or the path that shows otherwise
284
+ **Fix:** What the document should say, or the section that has to be added.
285
+ ```
286
+
287
+ No bullet lists and no user stories. Do not write any issue number (`#123`) in the report: the workflow adds the links to the stories, and every number in it is read as work the report waits on.
261
288
 
262
- The issue goes to Refine, which sizes it and splits it into one issue per finding, so write
263
- each finding to survive that: self-contained, naming its own files, never "as above".
289
+ 9. File the stories: write the top ${{ env.DOCS_AUDIT_MAX_STORIES }} findings as short stories. Call `create_issue` once per story, after the report, titled with what the story fixes (never "Docs audit: ..."), no labels. Refine refines each story on its own, so each must stand alone: it names its document and the code that contradicts it, says what the document should say, and never says "as above" or mentions the other stories or the report. Keep each story well under ${{ env.DOCS_AUDIT_MAX_BODY_CHARS }} characters and ${{ env.DOCS_AUDIT_MAX_WORK_UNITS }} bullet lines: the workflow measures every issue against that limit before filing and cuts anything over it from the end.
264
290
 
265
291
  Refer to people by role, never by name, and include no personal contact details, even when
266
292
  a document you are quoting contains them.
@@ -47,7 +47,8 @@ env:
47
47
  # one, Pliny-Bot #305 (13,398 chars, 92 work units), burned a full agent -- 17 minutes,
48
48
  # 2.4M tokens -- and died at the provider's 32K output cap with zero outcome, twice.
49
49
  # Both numbers are measured before the agent starts, so refusal is deterministic and
50
- # costs ten seconds instead of half an hour.
50
+ # costs ten seconds instead of half an hour. The audit files under its own AUDIT_MAX_*,
51
+ # which must stay below these or refine refuses what it files.
51
52
  REFINE_MAX_BODY_CHARS: "8000"
52
53
  REFINE_MAX_WORK_UNITS: "24"
53
54
  REFUSE_MARKER: "<!-- agent-refine-refused -->"
@@ -207,9 +208,8 @@ jobs:
207
208
  # The size gate, rung 4. Measures the issue before any model starts, because an oversized
208
209
  # body does not fail fast on its own: it produces an agent that explores for seventeen
209
210
  # minutes and then dies mid-generation at the provider's response cap having written
210
- # nothing, which reads as a green run with no outcome (Pliny-Bot #305, twice). Pure
211
- # shell, no network beyond one issue read; over either limit it is a refusal, not a
212
- # smaller attempt.
211
+ # nothing, which reads as a green run with no outcome (Pliny-Bot #305, twice). No
212
+ # model, one issue read; over either limit it is a refusal, not a smaller attempt.
213
213
  size_guard:
214
214
  needs: [still_open, eligibility]
215
215
  if: needs.still_open.outputs.open == 'true' && needs.eligibility.outputs.eligible == 'true'
@@ -219,36 +219,23 @@ jobs:
219
219
  contents: read
220
220
  issues: read
221
221
  outputs:
222
- too_big: ${{ steps.measure.outputs.too_big }}
222
+ too_big: ${{ steps.measure.outputs.too-big }}
223
223
  reason: ${{ steps.measure.outputs.reason }}
224
224
  steps:
225
+ - name: Checkout workflow actions
226
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
227
+ with:
228
+ persist-credentials: false
229
+ # The rule lives in issue-size.cjs, which the audit also runs before it files, so the two
230
+ # cannot drift: every audit report from 2026-09-21 on was refused here for want of it.
225
231
  - name: Measure the issue against the size limits
226
232
  id: measure
227
- env:
228
- GH_TOKEN: ${{ github.token }}
229
- REPO: ${{ github.repository }}
230
- ISSUE_NUMBER: ${{ inputs.issue-number }}
231
- MAX_BODY_CHARS: ${{ env.REFINE_MAX_BODY_CHARS }}
232
- MAX_WORK_UNITS: ${{ env.REFINE_MAX_WORK_UNITS }}
233
- run: |
234
- set -euo pipefail
235
- body=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title,body --jq '.title + "\n\n" + (.body // "")')
236
- chars=$(printf '%s' "$body" | wc -c)
237
- # One work unit per markdown bullet, the same split the prompt's step 3 makes.
238
- units=$(printf '%s' "$body" | grep -cE '^[[:space:]]*[-*] ' || true)
239
- reason=""
240
- too_big=false
241
- if [ "$chars" -gt "$MAX_BODY_CHARS" ]; then
242
- too_big=true
243
- reason="body ${chars} chars > ${MAX_BODY_CHARS}"
244
- fi
245
- if [ "$units" -gt "$MAX_WORK_UNITS" ]; then
246
- too_big=true
247
- reason="${reason:+$reason; }work units ${units} > ${MAX_WORK_UNITS}"
248
- fi
249
- echo "too_big=$too_big" >> "$GITHUB_OUTPUT"
250
- echo "reason=$reason" >> "$GITHUB_OUTPUT"
251
- echo "::notice::measured $chars chars, $units work units (limits: ${MAX_BODY_CHARS} chars, ${MAX_WORK_UNITS} units) -> $too_big"
233
+ uses: ./.github/actions/measure-issue-size
234
+ with:
235
+ token: ${{ github.token }}
236
+ issue-number: ${{ inputs.issue-number }}
237
+ max-body-chars: ${{ env.REFINE_MAX_BODY_CHARS }}
238
+ max-work-units: ${{ env.REFINE_MAX_WORK_UNITS }}
252
239
  # The deterministic refusal. Runs only when the size gate tripped, so it costs one
253
240
  # comment and no model. The refine label stays: a human who shrinks the body and
254
241
  # replies re-enters rerefine through the comment route, and the classifier ignores
@@ -151,6 +151,14 @@ env:
151
151
  # The single issue that lists what genuinely needs a person. Set it empty to turn the digest
152
152
  # off and keep the rest of the janitor.
153
153
  HOUSEKEEPING_DIGEST_TITLE: "Needs a human"
154
+ # Which Dependabot pull requests the six-hourly maintenance squash-merges once every check is
155
+ # green: "all", "minor" (minor and patch), "patch", or "off". CI is the gate; only pull requests
156
+ # Dependabot opened, with every commit Dependabot's, are touched. A repository variable of the
157
+ # same name overrides this without a commit.
158
+ DEPENDABOT_MERGE: "all"
159
+ # "true" also merges a Dependabot pull request in a repository with no checks at all, once its
160
+ # head is an hour old. "false" leaves those for a person.
161
+ DEPENDABOT_MERGE_WITHOUT_CHECKS: "true"
154
162
  # How long an audit report may sit open before it is labelled `stale-audit`. The audit's own
155
163
  # backpressure ignores a stale report, so three abandoned reports can no longer disable the
156
164
  # weekly audit for good; the digest lists them instead. Two missed weekly cycles.
@@ -1827,6 +1835,9 @@ jobs:
1827
1835
  pull-requests: read
1828
1836
  # actions: write cancels workflow runs stuck pending in the fleet queue.
1829
1837
  actions: write
1838
+ # The Dependabot merge reads check runs and statuses with this token, not the App's.
1839
+ checks: read
1840
+ statuses: read
1830
1841
  env:
1831
1842
  BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
1832
1843
  BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
@@ -1852,6 +1863,18 @@ jobs:
1852
1863
  stale-pr-days: ${{ env.HOUSEKEEPING_STALE_PR_DAYS }}
1853
1864
  digest-title: ${{ env.HOUSEKEEPING_DIGEST_TITLE }}
1854
1865
  dry-run: ${{ github.event.inputs.dry-run || 'false' }}
1866
+ # Dependabot updates merge here rather than on a cron of their own. API calls only: no pull
1867
+ # request code is checked out. It runs even when the sweep above failed.
1868
+ - name: Merge Dependabot pull requests
1869
+ if: ${{ !cancelled() && steps.app-token.outcome == 'success' }}
1870
+ uses: ./.github/actions/merge-dependabot
1871
+ with:
1872
+ token: ${{ steps.app-token.outputs.token }}
1873
+ read-token: ${{ github.token }}
1874
+ scope: ${{ vars.DEPENDABOT_MERGE || env.DEPENDABOT_MERGE }}
1875
+ merge-without-checks: ${{ env.DEPENDABOT_MERGE_WITHOUT_CHECKS }}
1876
+ skip-labels: do-not-merge blocked human-review ${{ env.MERGE_TRAIN_HOLD_LABEL }}
1877
+ dry-run: ${{ github.event.inputs.dry-run || 'false' }}
1855
1878
 
1856
1879
  validate:
1857
1880
  needs: classify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -41,6 +41,6 @@
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.0.0",
43
43
  "typescript": "^5.8.0",
44
- "vitest": "^3.0.0"
44
+ "vitest": "^4.1.11"
45
45
  }
46
46
  }