@plainconceptsplatform/workflows 0.28.9 → 0.28.11

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.
@@ -15,6 +15,7 @@ const sourceMappings = [
15
15
  ["actions", ".github/actions"],
16
16
  ["workflows", ".github/workflows"],
17
17
  ["scripts", "scripts"],
18
+ ["docs", "docs"],
18
19
  ];
19
20
  export function mandatoryFileSpecs(sourcePath) {
20
21
  return mandatoryFiles.map((spec) => ({
@@ -183,7 +184,7 @@ function hasEmptyVerifyCommands(current) {
183
184
  // consumer forever: that is how `stale-recovery` and `update-changelog` outlived the code that
184
185
  // called them. Deliberately not `.github/workflows/`, where a worker's absence means the route
185
186
  // is not installed rather than gone, and never anything without a header, which is a fork.
186
- const pruneRoots = [".github/actions", ".github/workflows/shared"];
187
+ const pruneRoots = [".github/actions", ".github/workflows/shared", "docs"];
187
188
  async function orphanedManagedFiles(repositoryPath, keep) {
188
189
  const orphans = [];
189
190
  for (const root of pruneRoots) {
@@ -7,7 +7,7 @@ export interface WorkflowRoute {
7
7
  readonly defaultEnabled: boolean;
8
8
  }
9
9
  export declare const workflowRoutes: readonly WorkflowRoute[];
10
- export declare const packageOwnedTargets: readonly [".github/actions", ".github/workflows/agent-*.md", ".github/workflows/shared/platform-defaults.md", ".github/workflows/shared/opencode-ci.md", ".github/workflows/work-router.yml", "scripts/compile-agent-workflows.mjs", "opencode.ci.json"];
10
+ export declare const packageOwnedTargets: readonly [".github/actions", ".github/workflows/agent-*.md", ".github/workflows/shared/platform-defaults.md", ".github/workflows/shared/opencode-ci.md", ".github/workflows/work-router.yml", "scripts/compile-agent-workflows.mjs", "opencode.ci.json", "docs/diagrams.md"];
11
11
  export interface MandatoryFile {
12
12
  readonly source: string;
13
13
  readonly target: string;
@@ -24,6 +24,7 @@ export const packageOwnedTargets = [
24
24
  ".github/workflows/work-router.yml",
25
25
  "scripts/compile-agent-workflows.mjs",
26
26
  "opencode.ci.json",
27
+ "docs/diagrams.md",
27
28
  ];
28
29
  export const mandatoryFiles = [
29
30
  { source: "templates/opencode/opencode.ci.json", target: "opencode.ci.json" },
@@ -0,0 +1,659 @@
1
+ ---
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/docs/diagrams.md. Update with workflows update --force; consumer edits may be overwritten.
3
+ ---
4
+ # Diagrams
5
+
6
+ One picture per route, plus the router that chooses between them.
7
+
8
+ These used to live at the bottom of each worker's markdown file. Everything below a worker's
9
+ frontmatter is the prompt, so each diagram was sent to the model on every run and then explicitly
10
+ skipped by a closing instruction telling it to ignore the section. They were about 240 lines across
11
+ the eight workers, and in the release worker's case 21 of its 48 prompt lines. They are documentation
12
+ for people, so they live here, and `verify-route-matrix.sh` fails if one reappears in a prompt.
13
+
14
+ Every diagram uses the same shapes and the same palette, so reading one teaches you the rest:
15
+
16
+ | Shape | Means |
17
+ |---|---|
18
+ | Rounded, indigo | a job that does something |
19
+ | Square, amber | a decision the workflow or the agent makes |
20
+ | Doubled circle, green | a terminal state that made progress |
21
+ | Doubled circle, red | a terminal state that needs a person |
22
+ | Doubled circle, dark | nothing to do, the run ends idle |
23
+ | Dotted arrow | the unhappy path |
24
+
25
+ Each worker diagram also names the rung each stage sits on, from the determinism ladder in
26
+ `skills/workflow-author`: the lower the rung, the cheaper and more reproducible the decision.
27
+
28
+ ---
29
+
30
+ ## The router
31
+
32
+ Every trigger the repository has arrives here, and exactly one thing happens per event. Nothing
33
+ else in the system subscribes to a public event, which is what makes a route addable or removable
34
+ without touching the others.
35
+
36
+ ```mermaid
37
+ flowchart TD
38
+ ev{"One GitHub event"} --> classify
39
+ classify["classify (rung 1)<br/>pure shell, no network<br/>one event in, one route out"] --> authorize
40
+ authorize{"authorize (rung 1)<br/>Write permission, and is the<br/>actor one of the org's own?"}
41
+ authorize -->|"human, trusted"| work
42
+ authorize -->|"outside collaborator"| triageOnly
43
+ authorize -.->|"no permission"| idle
44
+ work{"route"} -->|refine| wRefine
45
+ work -->|implement| wImpl
46
+ work -->|apply-review| wReview
47
+ work -->|merge-gate| wGate
48
+ work -->|audit| wAudit
49
+ work -->|release| wRelease
50
+ triageOnly("dispatch-triage<br/>re-enters as the App, so the<br/>worker sees a trusted actor") --> wTriage
51
+ wRefine("agent-refine")
52
+ wImpl("agent-implement")
53
+ wReview("agent-apply-review")
54
+ wGate("agent-merge-gate<br/>one lock for the whole repo")
55
+ wAudit("agent-audit")
56
+ wRelease("agent-release")
57
+ wTriage("agent-triage")
58
+ classify -.->|"cron or dispatch"| plumbing
59
+ plumbing["deterministic jobs<br/>no model runs in any of these"] --> pJobs
60
+ pJobs("bot-approve · audit-close<br/>cleanup-artifacts · validate<br/>reconcile-bot-pr-runs · detect-pr-conflicts")
61
+ idle(("Idle<br/>run ends, ~10s, every job skipped"))
62
+
63
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
64
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
65
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
66
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
67
+ class ev start
68
+ class classify,triageOnly,plumbing,pJobs,wRefine,wImpl,wReview,wGate,wAudit,wRelease,wTriage action
69
+ class authorize,work decision
70
+ class idle idle
71
+ ```
72
+
73
+ A run still appears for every matching event even when the router decides nothing downstream
74
+ happens; those cost about ten seconds with every job skipped. Reducing the count means generating
75
+ fewer events, not adding more guards.
76
+
77
+ ### How the routes chain
78
+
79
+ ```mermaid
80
+ flowchart LR
81
+ open("Issue opened") --> triage
82
+ triage{"triage<br/>outside collaborator"} -->|pass| refine
83
+ triage -.->|block| closed(("Closed"))
84
+ triage -.->|needs-info| author("Author replies") --> triage
85
+ write("Write+ user<br/>self-labels") --> refine
86
+ refine{"refine<br/>estimate in points"} -->|"5 or less"| implement
87
+ refine -->|"8 or more"| split("Split into children") --> refine
88
+ refine -.->|questions| author
89
+ implement("implement<br/>branch, PR, closes the issue") --> ci("CI")
90
+ ci --> gate{"merge-gate"}
91
+ gate -->|merge| merged(("Merged"))
92
+ gate -.->|"CI failed"| fix("Fix, push, CI again") --> gate
93
+ gate -.->|"risk or protected"| human(("Human review"))
94
+ review("Someone reviews the PR") --> applyReview("apply-review") --> ci
95
+ audit("audit, weekly") -->|"files one issue"| refine
96
+
97
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
98
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
99
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
100
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
101
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
102
+ class open,write,review,audit start
103
+ class refine,split,implement,ci,fix,applyReview,author action
104
+ class triage,gate decision
105
+ class human,closed failure
106
+ class merged success
107
+ ```
108
+
109
+ Audit creates work rather than consuming it, and files into `refine` rather than straight to
110
+ `implement`, so a report of several unrelated findings becomes one properly sized issue per finding
111
+ instead of one pull request that has to fix them all.
112
+
113
+ ---
114
+
115
+ ## agent-triage.md
116
+
117
+ The front door for issues opened from outside the organisation. The gate is membership, not permission
118
+ level: an org member with write skips triage and self-labels into the pipeline, while an outside
119
+ collaborator goes through it even when they hold write.
120
+
121
+ Four outcomes, and only one of them closes anything. `needs-maintainer` is the one worth knowing: the
122
+ request is legitimate but addressed to the wrong intake, so the issue stays open with `review` and a
123
+ maintainer takes it on by adding `refine`.
124
+
125
+ ```mermaid
126
+ flowchart TD
127
+ triStart("Work Router<br/>triage route") --> triPick
128
+ triPick{"Opened by an<br/>outside collaborator?"} -->|yes| triReserve
129
+ triPick -.->|"no, write+ user"| triIdle
130
+ triReserve("Reserve (rung 4)<br/>bot-working + triage") --> triFacts
131
+ triFacts("Facts (rung 3)<br/>Issue, comments and every open<br/>issue written to disk") --> triRound
132
+ triRound["Round N of 3<br/>counted from the markers<br/>already on the issue"] --> triAgent
133
+ triAgent("Agent (rung 5)<br/>10 checks: template, security, size,<br/>danger, duplicates, clarity, repro,<br/>acceptance, cross-cutting, product scope") --> triValidate
134
+ triValidate{"Outcome valid?"} -->|yes| triOutcome
135
+ triValidate -.->|no| triIncomplete
136
+ triOutcome{"Verdict"} -->|pass| triPass
137
+ triOutcome -->|needs-info| triReview
138
+ triOutcome -->|needs-maintainer| triMaintainer
139
+ triOutcome -->|block| triBlocked
140
+ triRound -.->|"round 3: needs-info<br/>is no longer allowed"| triAgent
141
+ triPass(("Passed<br/>refine added, triage removed<br/>enters the pipeline, no human"))
142
+ triReview(("Needs info<br/>questions posted, review added<br/>triage kept, so a reply re-runs it"))
143
+ triReview -->|"author or write+ replies<br/>re-enters via the router"| triStart
144
+ triMaintainer(("Needs a maintainer<br/>out of product-owner scope, so it stays<br/>OPEN with review; triage removed.<br/>Add refine to take it on"))
145
+ triBlocked(("Blocked<br/>cannot be done, unsafe, or still<br/>ambiguous: closed with a reason"))
146
+ triIdle(("Idle<br/>write+ user, skipped"))
147
+ triIncomplete(("Incomplete<br/>review added, label kept for a retry"))
148
+
149
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
150
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
151
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
152
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
153
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
154
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
155
+ class triStart start
156
+ class triReserve,triFacts,triAgent action
157
+ class triPick,triRound,triValidate,triOutcome decision
158
+ class triIdle idle
159
+ class triIncomplete,triBlocked failure
160
+ class triPass,triReview,triMaintainer success
161
+ ```
162
+
163
+ ---
164
+
165
+ ## agent-refine.md
166
+
167
+ Decides the size of the work, which is the decision that determines whether it ever lands. An
168
+ estimate of 8 or more is split into children of 5 or less, and each child walks the pipeline alone.
169
+ The refined story is wrapped in the repository's own issue form, and when questions remain the
170
+ worker leaves a temporal draft on the issue and asks everything in one batched comment.
171
+
172
+ ```mermaid
173
+ flowchart TD
174
+ refStart("Work Router<br/>refine route") --> refReserve
175
+ refReserve("Reserve (rung 4)<br/>bot-working") --> refFacts
176
+ refFacts("Facts (rung 3)<br/>Issue and comments to disk") --> refExplore
177
+ refExplore("Explore (rung 5)<br/>one work unit at a time,<br/>self-answer from the code,<br/>at most 5 questions each") --> refClassify
178
+ refClassify{"Trivial?<br/>every TRIVIAL_CRITERIA<br/>condition holds"}
179
+ refClassify -->|yes| refTrivial
180
+ refClassify -->|no| refStory
181
+ refTrivial("Trivial plan<br/>marker, summary, checklist<br/>no Gherkin, no diagram") --> refEstimate
182
+ refStory("Story<br/>Given/When/Then per work unit,<br/>grounded in the code") --> refWrap
183
+ refStory -.->|"cannot ground it"| refFail
184
+ refWrap("Wrap<br/>story into the repository's<br/>own issue form") --> refProse
185
+ refProse("Prose<br/>@humanizer over the final text") --> refEstimate
186
+ refEstimate["Estimate<br/>Fibonacci, against ESTIMATE_BANDS"] --> refSplit
187
+ refSplit{"8 or more?"}
188
+ refSplit -->|no| refOutcome
189
+ refSplit -->|"yes, and it splits"| refChildren
190
+ refSplit -->|"yes, indivisible"| refOutcome
191
+ refChildren(("Split<br/>2-6 children, each 5 or less<br/>parent becomes their tracker"))
192
+ refOutcome{"Questions left<br/>for the author?"}
193
+ refOutcome -->|no| refDone
194
+ refOutcome -->|yes| refAsk
195
+ refDone(("Refined<br/>estimate recorded, implement added"))
196
+ refAsk(("Questions<br/>temporal draft on the issue,<br/>one batched comment,<br/>review added"))
197
+ refAsk -->|"author replies<br/>re-enters via the router"| refStart
198
+ refFail(("Incomplete<br/>refine label kept for a retry"))
199
+
200
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
201
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
202
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
203
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
204
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
205
+ class refStart start
206
+ class refReserve,refFacts,refExplore,refTrivial,refStory,refWrap,refProse action
207
+ class refClassify,refEstimate,refSplit,refOutcome decision
208
+ class refFail failure
209
+ class refDone,refAsk,refChildren success
210
+ ```
211
+
212
+ ---
213
+
214
+ ## agent-implement.md
215
+
216
+ Writes the code and opens one pull request. It stops there: the merge decision belongs to the gate,
217
+ because waiting for CI inside this run would hold a fleet machine doing nothing.
218
+
219
+ ```mermaid
220
+ flowchart TD
221
+ implStart("Work Router<br/>implement route<br/>router already confirmed<br/>no open PR for this issue") --> implEligible
222
+ implEligible{"eligibility (rung 4)<br/>Issue still open?<br/>Not labelled future?"}
223
+ implEligible -.->|no| implIdle
224
+ implEligible -->|yes| implReserve
225
+ implReserve("Reserve (rung 4)<br/>bot-working") --> implFacts
226
+ implFacts("Facts (rung 3)<br/>Issue and comments to disk") --> implCheck
227
+ implCheck{"Trivial marker<br/>left by refine?"}
228
+ implCheck -->|yes| implTodos
229
+ implCheck -->|no| implCode
230
+ implTodos("Direct path<br/>one todo per checklist item") --> implVerify
231
+ implCode("Standard path<br/>the pc-plan-goal pipeline:<br/>explore, propose, apply, verify,<br/>archive; skips ahead when the<br/>issue is already refined") --> implVerify
232
+ implVerify{"Verify<br/>scoped to the changed files,<br/>because a whole-repo run<br/>gets OOM-killed"}
233
+ implVerify -.->|fails| implCode
234
+ implVerify -->|passes| implPr
235
+ implPr("create_pull_request<br/>one safe output, complete first time") --> implHandoff
236
+ implPr -.->|"run died"| implRetry
237
+ implRetry{"Died under 6 minutes<br/>with no answer?"}
238
+ implRetry -->|"yes: provider outage"| implAgain(("Retry<br/>attempt N of 5, label kept"))
239
+ implRetry -.->|"no: it answered, wrongly"| implFail
240
+ implAgain -->|"re-enters via the router"| implStart
241
+ implHandoff(("Handed off<br/>PR open, bot-working removed,<br/>the gate decides next"))
242
+ implIdle(("Idle<br/>closed, or held by future"))
243
+ implFail(("Parked<br/>review added, a rerun<br/>would fail the same way"))
244
+
245
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
246
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
247
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
248
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
249
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
250
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
251
+ class implStart start
252
+ class implReserve,implFacts,implTodos,implCode,implPr action
253
+ class implEligible,implCheck,implVerify,implRetry decision
254
+ class implIdle idle
255
+ class implFail failure
256
+ class implHandoff,implAgain success
257
+ ```
258
+
259
+ The retry belt exists because a provider outage kills a run in a couple of minutes with no answer,
260
+ and that used to burn the issue and hand it to a human. A run that worked for half an hour and then
261
+ failed produced an answer that was wrong; repeating it costs the whole fleet the same half hour to
262
+ be wrong again, so only the short deaths are retried.
263
+
264
+ ---
265
+
266
+ ## agent-merge-gate.md
267
+
268
+ The only worker that merges. It runs one at a time for the whole repository, because several
269
+ overnight pull requests mean every merge moves the default branch under the rest.
270
+
271
+ ```mermaid
272
+ flowchart TD
273
+ gateStart("Work Router<br/>merge-gate route<br/>CI reported, or the hourly<br/>reconcile belt dispatched it") --> gateSubject
274
+ gateSubject{"subject (rung 4)<br/>Our open PR?<br/>Closes an implement issue?"}
275
+ gateSubject -.->|no| gateIdle
276
+ gateSubject -->|yes| gateBranch
277
+ gateBranch("Check out the PR branch<br/>the push is fast-forward only,<br/>so never rebase") --> gateFacts
278
+ gateFacts("Facts (rung 3)<br/>Diff, PR shape, failing CI logs") --> gateCi
279
+ gateCi{"What did CI conclude?"}
280
+ gateCi -->|success| gateProtected
281
+ gateCi -->|failure| gateFix
282
+ gateFix("Repair<br/>read the failure, fix, verify.<br/>Empty evidence on a conflicting<br/>PR means the conflict is the fault") --> gatePush
283
+ gatePush(("Pushed<br/>CI runs again, gate re-enters"))
284
+ gateFix -.->|"cannot fix"| gateHuman
285
+ gateProtected{"protected_changes (rung 4b)<br/>blast radius from paths,<br/>CODEOWNERS and diff shape"}
286
+ gateProtected -.->|"protected path"| gateOwner
287
+ gateProtected -->|"measured low, medium or high"| gateAssess
288
+ gateAssess("Agent (rung 5)<br/>find defects, verify them,<br/>rate recoverability") --> gateDisposition
289
+ gateDisposition{"Disposition<br/>computed in shell from<br/>the facts and the report"}
290
+ gateDisposition -->|"no verified blocker,<br/>radius low or recoverable medium"| gateMerge
291
+ gateDisposition -.->|"radius high, protected<br/>or owner path"| gateOwner
292
+ gateDisposition -.->|"fragile, unmet criteria<br/>or low confidence"| gateHuman
293
+ gateDisposition -.->|"CI red or a verified<br/>high finding"| gateBlocked
294
+ gateMerge(("AUTO-MERGE<br/>squash, issue closed,<br/>pr-pending removed"))
295
+ gateHuman(("HUMAN REVIEW<br/>review label,<br/>implement label kept"))
296
+ gateOwner(("OWNER REVIEW<br/>review + owner-review,<br/>CODEOWNERS asked when named"))
297
+ gateBlocked(("BLOCKED<br/>review + blocked,<br/>the belt does not retry"))
298
+ gateIdle(("Idle<br/>not ours, or not open"))
299
+ gateFix -.->|"attempt 6 of 6"| gateBlocked
300
+
301
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
302
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
303
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
304
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
305
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
306
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
307
+ class gateStart start
308
+ class gateBranch,gateFacts,gateFix,gateAssess action
309
+ class gateSubject,gateCi,gateProtected,gateDisposition decision
310
+ class gateIdle idle
311
+ class gateHuman,gateOwner,gateBlocked failure
312
+ class gateMerge,gatePush success
313
+ ```
314
+
315
+ A protected-path match holds the merge but does not stop the repair path: the agent may still fix
316
+ failed CI on those files, and `conclude` is what refuses to merge them.
317
+
318
+ The agent does not pick the disposition. It reports what it found and what it verified; the
319
+ validator computes the outcome from that report and from the blast radius measured before the
320
+ agent ran. A category the diff touches is no longer, on its own, a reason to park a pull request:
321
+ only a verified high or critical finding, a sensitive path, or a change that would be hard to undo
322
+ sends it to a person.
323
+
324
+ ---
325
+
326
+ ## agent-apply-review.md
327
+
328
+ Applies a reviewer's feedback to a pull request the bot already opened, and pushes to the same
329
+ branch.
330
+
331
+ ```mermaid
332
+ flowchart TD
333
+ fbStart("Work Router<br/>apply-review route<br/>review or comment on a bot PR") --> fbSubject
334
+ fbSubject{"subject (rung 4)<br/>Our open PR?<br/>Reviewer has write?"}
335
+ fbSubject -.->|no| fbIdle
336
+ fbSubject -->|yes| fbFacts
337
+ fbFacts("Facts (rung 3)<br/>Every thread with its resolved state,<br/>every inline comment, the diff") --> fbTriage
338
+ fbTriage{"Anything actionable<br/>and still outstanding?<br/>Account for every PRRT_ id"}
339
+ fbTriage -.->|"nothing, or all addressed"| fbSatisfied
340
+ fbTriage -->|yes| fbApply
341
+ fbApply("Apply<br/>only what the feedback justifies:<br/>a comment is not licence to refactor") --> fbVerify
342
+ fbVerify{"Verify<br/>scoped to the changed files"}
343
+ fbVerify -.->|fails| fbApply
344
+ fbVerify -->|passes| fbPush
345
+ fbApply -.->|"ambiguous or unsafe"| fbHuman
346
+ fbPush(("Implemented<br/>pushed to the same branch,<br/>CI runs, the gate re-enters"))
347
+ fbSatisfied(("Already satisfied<br/>nothing pushed, the reviewer<br/>has to confirm"))
348
+ fbHuman(("Needs a human<br/>review added"))
349
+ fbIdle(("Idle<br/>not ours, or not a write reviewer"))
350
+
351
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
352
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
353
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
354
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
355
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
356
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
357
+ class fbStart start
358
+ class fbFacts,fbApply action
359
+ class fbSubject,fbTriage,fbVerify decision
360
+ class fbIdle idle
361
+ class fbHuman failure
362
+ class fbPush,fbSatisfied success
363
+ ```
364
+
365
+ A reviewer often makes one point across several comments, so the whole conversation is on disk
366
+ before anything changes. Applying them one at a time produces contradictory commits.
367
+
368
+ ---
369
+
370
+ ## agent-audit.md
371
+
372
+ The only worker that creates work rather than consuming it. Read-only: it files an issue and
373
+ changes nothing.
374
+
375
+ ```mermaid
376
+ flowchart TD
377
+ auStart("Work Router<br/>audit route<br/>weekly cron, or dispatch") --> auBack
378
+ auBack{"Backpressure (rung 1)<br/>Fewer than 3 open reports?"}
379
+ auBack -.->|no| auIdle
380
+ auBack -->|yes| auFacts
381
+ auFacts("Facts (rung 3)<br/>Every open issue's title<br/>and labels to disk") --> auRun
382
+ auRun("Audit (rung 5)<br/>/repo-audit over AUDIT_FOCUS,<br/>read-only throughout") --> auFilter
383
+ auFilter{"Each finding: specific,<br/>reproducible, real impact,<br/>fixable without more digging?"}
384
+ auFilter -->|keeps some| auScore
385
+ auFilter -.->|keeps none| auQuiet
386
+ auScore("Score 1-10<br/>severity, likelihood, blast radius") --> auDedupe
387
+ auDedupe{"Already tracked, or<br/>previously rejected?"}
388
+ auDedupe -.->|"all of them"| auQuiet
389
+ auDedupe -->|"some are new"| auFile
390
+ auFile("One issue: every finding,<br/>top 3 refined into stories") --> auReport
391
+ auReport(("Filed<br/>labelled refine, so it gets<br/>sized and split per finding"))
392
+ auQuiet(("Nothing to file<br/>the right outcome on a<br/>clean codebase"))
393
+ auIdle(("Idle<br/>backlog already full"))
394
+
395
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
396
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
397
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
398
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
399
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
400
+ class auStart start
401
+ class auFacts,auRun,auScore,auFile action
402
+ class auBack,auFilter,auDedupe decision
403
+ class auIdle idle
404
+ class auReport,auQuiet success
405
+ ```
406
+
407
+ Memory matters here: open issues only cover what is still open, so a finding reported weeks ago and
408
+ consciously not acted on would come back every single run. The audit records its dispositions and
409
+ reads them next time.
410
+
411
+ ---
412
+
413
+ ## agent-release.md
414
+
415
+ Manual dispatch only. The agent writes prose; a deterministic job does everything irreversible.
416
+
417
+ ```mermaid
418
+ flowchart TD
419
+ relStart("Work Router<br/>release route<br/>operation=release") --> relFacts
420
+ relFacts("Facts (rung 3)<br/>Commit log since the last tag<br/>and the current version, to disk") --> relAgent
421
+ relAgent("Agent (rung 5)<br/>Categorise by conventional-commit<br/>prefix, write release-notes.md.<br/>The only judgement in this route") --> relNoop
422
+ relNoop("noop<br/>the agent writes a file,<br/>not a GitHub object") --> relConclude
423
+ relConclude["conclude (rung 6)<br/>Bump: BREAKING is major,<br/>feat is minor, else patch"] --> relPush
424
+ relPush("Commit, tag, push,<br/>create the GitHub Release") --> relDone
425
+ relConclude -.->|fails| relFail
426
+ relDone(("Released<br/>tag and Release published"))
427
+ relFail(("Failed<br/>no tag created, nothing partial"))
428
+
429
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
430
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
431
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
432
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
433
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
434
+ class relStart start
435
+ class relFacts,relAgent,relNoop,relPush action
436
+ class relConclude decision
437
+ class relFail failure
438
+ class relDone success
439
+ ```
440
+
441
+ The version bump, the tag and the Release are deterministic on purpose. The agent's only output is
442
+ a file on disk, so a confused model cannot publish a release.
443
+
444
+
445
+ ---
446
+
447
+ ## agent-visual-verify.md
448
+
449
+ Runs after the merge gate decides auto-merge and before the merge itself. The agent reads the
450
+ change, writes a verification plan as JSON waypoints, and a post-agent shell step drives
451
+ agent-browser to capture screenshots at each waypoint. A final step attaches the screenshots
452
+ to the linked issue as a comment. Failures never block the merge.
453
+
454
+ ```mermaid
455
+ flowchart TD
456
+ vvStart("Merge Gate<br/>auto-merge verdict") --> vvSubject
457
+ vvSubject{"subject (rung 4)<br/>PR open? Visual<br/>verify enabled?"}
458
+ vvSubject -.->|"no"| vvIdle
459
+ vvSubject -->|"yes"| vvActivation
460
+ vvActivation("Activation (rung 3)<br/>Checkout, load issue context,<br/>build the prompt") --> vvAgent
461
+ vvAgent("Agent (rung 5)<br/>Explore the change, write<br/>plan.json with waypoints<br/>(url + selector per step)") --> vvBuild
462
+ vvBuild("Build and start app<br/>VISUAL_VERIFY_BUILD_COMMAND,<br/>then VISUAL_VERIFY_START_COMMAND<br/>on the configured port") --> vvAppUp{"App up on<br/>the port?"}
463
+ vvAppUp -.->|"no"| vvWarn
464
+ vvAppUp -->|"yes"| vvCapture
465
+ vvCapture["Capture screenshots<br/>agent-browser opens each waypoint,<br/>waits for selectors,<br/>screenshot --full --no-sandbox"] --> vvGotShots{"Any captured?"}
466
+ vvGotShots -->|"yes"| vvAttach
467
+ vvGotShots -.->|"no, all failed"| vvWarn
468
+ vvAttach("Attach to issue<br/>post plan + screenshots<br/>as a single comment") --> vvDone
469
+ vvDone(("Screenshots posted<br/>visual evidence on the issue"))
470
+ vvWarn(("Warning posted<br/>no screenshots, merge proceeds"))
471
+ vvIdle(("Idle<br/>not enabled or not our PR"))
472
+
473
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
474
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
475
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
476
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
477
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
478
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
479
+ class vvStart start
480
+ class vvActivation,vvAgent,vvBuild,vvCapture,vvAttach action
481
+ class vvSubject,vvAppUp,vvGotShots decision
482
+ class vvIdle idle
483
+ class vvWarn failure
484
+ class vvDone success
485
+ ```
486
+
487
+ The agent does not take screenshots itself. It writes a plan; a shell step runs agent-browser
488
+ against the running app. Chromium needs --no-sandbox on CI runners (set via
489
+ AGENT_BROWSER_ARGS), and the explicit apt-get install covers libraries gent-browser
490
+ install --with-deps misses on ubuntu-24.04.
491
+
492
+ ---
493
+
494
+ ## housekeeping
495
+
496
+ Not a worker: a deterministic job in the router, on its own six-hourly cron, with no model. It is
497
+ the answer to a specific measured problem. Across the four consumers, 58 of 237 issues closed since
498
+ 1 August were closed by the bot; autonomy ran between 6% and 50%. The gap was not bad decisions, it
499
+ was silence — work that stopped and told nobody, and state that nothing ever cleaned up.
500
+
501
+ The one rule that shapes it: **retry a failure, report a decision.** A crash, a timeout or an empty
502
+ output is a machine failure, and the worker marks it `stalled`; those are worth running again. A
503
+ triage `needs-maintainer`, a refine `questions` or a merge-gate `review` is a verdict the agent
504
+ reached on purpose, and running it again just reproduces it.
505
+
506
+ ```mermaid
507
+ flowchart TB
508
+ hkCron("cron 23 */6 * * *") --> hkScan("List open issues,<br/>pull requests and branches")
509
+ hkScan --> hkPark{"Issue carries<br/>review?"}
510
+ hkPark -.->|no| hkStrand
511
+ hkPark -->|yes| hkWhy{"and stalled?"}
512
+ hkWhy -.->|"no: a decision"| hkDigest
513
+ hkWhy -->|"yes: a machine failure"| hkBudget{"waited 6h,<br/>under 3 tries?"}
514
+ hkBudget -.->|"budget spent"| hkDigest
515
+ hkBudget -->|yes| hkRetry("Comment the attempt,<br/>drop review and stalled,<br/>dispatch the work route")
516
+ hkRetry --> hkStrand
517
+
518
+ hkStrand("Strands: drop pr-pending<br/>where no open PR closes the issue") --> hkParent("Close a split parent<br/>once every child is closed")
519
+ hkParent --> hkBranch{"Branch whose PRs<br/>are all finished<br/>and all bot-authored?"}
520
+ hkBranch -->|yes| hkDelete("Delete the branch")
521
+ hkBranch -.->|"open PR, human PR,<br/>or the default branch"| hkDigest
522
+ hkDelete --> hkDigest
523
+ hkDigest("Rewrite one issue:<br/>Needs a human") --> hkEnd
524
+ hkEnd(("One place to look<br/>instead of four repos"))
525
+
526
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
527
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
528
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
529
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
530
+ class hkCron start
531
+ class hkScan,hkRetry,hkStrand,hkParent,hkDelete,hkDigest action
532
+ class hkPark,hkWhy,hkBudget,hkBranch decision
533
+ class hkEnd success
534
+ ```
535
+
536
+ Three things make it safe to leave running unattended. It uses the App token, because GitHub starts
537
+ no workflow run from an event raised with `GITHUB_TOKEN`, so with the default token every retry
538
+ would be a green no-op. Every write goes through one `act()` wrapper, which is the only place
539
+ `dry-run` is read, so there is no second path that forgets to check it. And it closes exactly two
540
+ kinds of issue — a split parent whose children are all done, and its own digest — which the route
541
+ matrix asserts by counting.
542
+
543
+ ---
544
+
545
+ ## agentics-app-errors
546
+
547
+ An optional template, installed only where there is an Application Insights resource to read, and
548
+ the mirror image of `agentics-error-report` below it. That one carries a *workflow* failure out of a
549
+ private consumer into the public package, so it is built to send almost nothing. This one reads the
550
+ *application's* own exceptions, and they are the private repository's own business: the issue is
551
+ filed beside the code that threw it and nothing travels anywhere.
552
+
553
+ It runs no model, for the reason that one does not: a model asked to summarise a stack trace
554
+ paraphrases it, and a paraphrased stack trace is a wrong issue that costs somebody an afternoon. The
555
+ agent work happens afterwards, on the belt, because the issue is labelled `bug` + `refine` and that
556
+ is the same handoff the audit already uses.
557
+
558
+ ```mermaid
559
+ flowchart TB
560
+ aeCron("cron 41 7 * * *") --> aeConfig{"infra/&lt;env&gt;.env<br/>present?"}
561
+ aeConfig -.->|no| aeIdle(("Job summary only<br/>warning, not a red run"))
562
+ aeConfig -->|yes| aeLogin("azure/login, OIDC<br/>on the matching Environment")
563
+ aeLogin --> aeQuery("AppExceptions, one table,<br/>grouped by ProblemId,<br/>sum(ItemCount) for sampling")
564
+ aeQuery --> aeFloor{"Above the floor,<br/>and any slot left<br/>under the ceiling?"}
565
+ aeFloor -.->|no| aeDefer("Named in the summary,<br/>picked up tomorrow")
566
+ aeFloor -->|yes| aeScrub("Mask GUIDs, emails,<br/>paths, query strings")
567
+ aeScrub --> aeFields{"Every field on<br/>the allowlist?"}
568
+ aeFields -.->|"no: the module changed"| aeStop(("Nothing filed<br/>run goes red"))
569
+ aeFields -->|yes| aeScan{"Leak scanner:<br/>a GUID, a secret,<br/>a path, an address?"}
570
+ aeScan -.->|"this one trips"| aeWithhold("Withheld, the others<br/>still filed, run goes red")
571
+ aeScan -->|clean| aeSeen{"Filed before?"}
572
+ aeSeen -.->|"closed and accepted"| aeLeave(("Left alone, for good"))
573
+ aeSeen -->|"open"| aeNote("A dated line added")
574
+ aeSeen -->|"closed, not accepted"| aeReopen("Reopened: it came back")
575
+ aeSeen -->|"never"| aeFile("New issue,<br/>bug + refine + app-error")
576
+ aeFile --> aeBelt(("On to refine"))
577
+ aeNote --> aeBelt
578
+ aeReopen --> aeBelt
579
+
580
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
581
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
582
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
583
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
584
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
585
+ classDef idle fill:#f4f4f6,stroke:#5b5b66,stroke-width:2px,color:#2b2b33
586
+ class aeCron start
587
+ class aeLogin,aeQuery,aeScrub,aeNote,aeReopen,aeFile,aeDefer action
588
+ class aeConfig,aeFloor,aeFields,aeScan,aeSeen decision
589
+ class aeStop,aeWithhold failure
590
+ class aeBelt success
591
+ class aeIdle,aeLeave idle
592
+ ```
593
+
594
+ Two failures, handled two different ways, and the difference is the point. A field nobody agreed to
595
+ publish means the module changed and the new field is probably on every finding, so nothing is filed
596
+ at all. A body that still trips the scanner after scrubbing is one unlucky row: that report is
597
+ withheld and the rest are filed, because one bad operation name must never mean the belt stops
598
+ hearing about anything. Both turn the run red.
599
+
600
+ Scrubbing comes before scanning for the same reason. Masking a GUID keeps the job alive where
601
+ refusing over one would have left it red every morning and filing nothing, which is how a workflow
602
+ gets switched off. The scanner is the backstop for whatever the masks missed, not the first line.
603
+
604
+ The route matrix asserts the query names one table and only inside the query itself, that counts are
605
+ summed rather than counted, that the reportable field set has not grown a run id, that the scrubs and
606
+ the scanner both still exist, that both `setFailed` paths are intact, and that no model has appeared.
607
+ Every one was mutation-tested. Four of the first drafts used the file's own `count()` helper inside
608
+ an `if`, and since that helper is `grep || true` those guards were true whatever they found: they
609
+ failed open, which is the exact bug this section exists to catch.
610
+
611
+ ---
612
+
613
+ ## agentics-error-report
614
+
615
+ An optional template, installed everywhere, and the only job in the fleet that sends anything out of
616
+ the repository it runs in. Every consumer is private, so that is the whole design constraint.
617
+
618
+ What crosses the boundary is a fixed, enumerable set of facts about workflows **this package ships**:
619
+ their file names, their job and step names, a conclusion, a runner label, a count, and the id of a
620
+ matched entry from a catalogue of sixteen known failure shapes. What never crosses it is free text
621
+ of any kind — no log line, no branch name, no issue title, no path, no URL, no number that could be
622
+ looked up. Hence no model: a model asked to summarise a failure paraphrases whatever the log held.
623
+
624
+ ```mermaid
625
+ flowchart TB
626
+ erCron("cron 11 7 * * *") --> erRuns("List this repo's runs<br/>from the last 24h")
627
+ erRuns --> erOwned{"Is the workflow one<br/>this package ships?"}
628
+ erOwned -.->|"no: the name could describe<br/>a product or a customer"| erCount("Counted, never inspected")
629
+ erOwned -->|yes| erState{"Failed, or queued<br/>past 45 minutes?"}
630
+ erState -.->|neither| erCount
631
+ erState -->|yes| erClassify("Read the log tail,<br/>match the catalogue,<br/>keep only the matched id")
632
+ erClassify --> erBody("Build the report from<br/>package-owned names,<br/>a conclusion and a count")
633
+ erBody --> erScan{"Leak scanner:<br/>repo or owner name, a URL,<br/>an email, a path, a token,<br/>an issue number, a ref?"}
634
+ erScan -.->|"anything matches"| erWithhold(("Withheld<br/>nothing filed, run goes red"))
635
+ erScan -->|clean| erToken{"Upstream token<br/>available?"}
636
+ erToken -.->|no| erSummary(("Job summary only<br/>warning, not a red run"))
637
+ erToken -->|yes| erFile("One upstream issue<br/>per signature, appended<br/>rather than repeated")
638
+ erFile --> erDone(("Filed upstream"))
639
+
640
+ classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
641
+ classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
642
+ classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
643
+ classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
644
+ classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
645
+ classDef idle fill:#f4f4f6,stroke:#5b5b66,stroke-width:2px,color:#2b2b33
646
+ class erCron start
647
+ class erRuns,erClassify,erBody,erFile,erCount action
648
+ class erOwned,erState,erScan,erToken decision
649
+ class erWithhold failure
650
+ class erDone success
651
+ class erSummary idle
652
+ ```
653
+
654
+ The scanner fails closed, and its failure is loud: a report it flags is not filed and the run goes
655
+ red, so the field that carried private text gets fixed rather than leaking again the next morning.
656
+ The route matrix asserts the scanner exists, that both exit paths call `setFailed`, that there are
657
+ at least as many scans as upstream writes, that each individual check is still present, and that no
658
+ model or `OPENAI_API_KEY` has appeared in the action. Every one of those was mutation-tested; three
659
+ earlier versions of them passed against a deliberately broken guard and were rewritten.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "0.28.9",
3
+ "version": "0.28.11",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",