@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;
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -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/<env>.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.
|