@plainconceptsplatform/workflows 0.28.12 → 0.28.14
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.
- package/loops/actions/housekeeping/action.yml +52 -0
- package/loops/docs/diagrams.md +49 -47
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/opencode/opencode.ci.json +24 -2
- package/loops/workflows/agent-apply-review.md +3 -0
- package/loops/workflows/agent-implement.md +3 -0
- package/loops/workflows/agent-merge-gate.md +4 -0
- package/loops/workflows/agent-refine.md +7 -0
- package/loops/workflows/agent-triage.md +2 -0
- package/loops/workflows/agent-visual-verify.md +1 -0
- package/loops/workflows/work-router.yml +2 -0
- package/package.json +4 -4
|
@@ -66,6 +66,26 @@ runs:
|
|
|
66
66
|
const age = (iso) => Date.now() - new Date(iso).getTime();
|
|
67
67
|
const days = (ms) => Math.floor(ms / 86400000);
|
|
68
68
|
|
|
69
|
+
// 0. Cancel workflow runs stuck in "pending" for over an hour. The ARC fleet is two
|
|
70
|
+
// VMs, and when it saturates, jobs sit pending indefinitely — GitHub has no queue-time
|
|
71
|
+
// timeout. A stuck pending implement or refine run holds a fleet slot that the retry
|
|
72
|
+
// logic below can never free because the run never reaches a terminal state. This
|
|
73
|
+
// cancel runs on a GitHub-hosted runner, so it works even when the fleet is full.
|
|
74
|
+
const stuckCutoff = Date.now() - 3600 * 1000; // 1 hour ago
|
|
75
|
+
let cancelledRuns = 0;
|
|
76
|
+
for (const run of await github.paginate(github.rest.actions.listWorkflowRuns, {
|
|
77
|
+
owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'work-router.yml',
|
|
78
|
+
status: 'pending', per_page: 50,
|
|
79
|
+
})) {
|
|
80
|
+
if (new Date(run.created_at).getTime() > stuckCutoff) continue;
|
|
81
|
+
await act(`cancelling stuck pending run #${run.id} (queued ${new Date(run.created_at).toISOString()})`, async () => {
|
|
82
|
+
await github.rest.actions.cancelWorkflowRun({
|
|
83
|
+
owner: context.repo.owner, repo: context.repo.repo, run_id: run.id,
|
|
84
|
+
});
|
|
85
|
+
cancelledRuns += 1;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
|
70
90
|
...context.repo, state: 'open', per_page: 100,
|
|
71
91
|
});
|
|
@@ -74,6 +94,37 @@ runs:
|
|
|
74
94
|
});
|
|
75
95
|
const names = (issue) => issue.labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
76
96
|
|
|
97
|
+
// 0b. Clean orphaned bot-working labels. When a workflow run is cancelled (above) or
|
|
98
|
+
// dies without clearing bot-working, the issue sits with bot-working + a work label
|
|
99
|
+
// forever: no stalled marker, so the retry logic below never touches it. Find issues
|
|
100
|
+
// with bot-working, check whether the most recent run for the linked operation is
|
|
101
|
+
// still active, and if not, park the issue for retry.
|
|
102
|
+
for (const issue of issues) {
|
|
103
|
+
if (issue.pull_request) continue;
|
|
104
|
+
const labels = names(issue);
|
|
105
|
+
if (!labels.includes('bot-working')) continue;
|
|
106
|
+
// Don't touch issues that already have stalled or review — those are handled below.
|
|
107
|
+
if (labels.includes('stalled') || labels.includes('review')) continue;
|
|
108
|
+
const work = WORK_LABELS.find((l) => labels.includes(l));
|
|
109
|
+
if (!work) continue;
|
|
110
|
+
|
|
111
|
+
// An issue with bot-working should have a recent or active workflow run.
|
|
112
|
+
// If bot-working has been on for longer than the retry window, the run is dead.
|
|
113
|
+
const waited = age(issue.updated_at);
|
|
114
|
+
if (waited < retryAfterMs) continue;
|
|
115
|
+
|
|
116
|
+
await act(`#${issue.number}: releasing orphaned bot-working (parked ${Math.floor(waited / 3600000)}h with no active run)`, async () => {
|
|
117
|
+
// Park it for retry: add stalled + review, remove bot-working.
|
|
118
|
+
// The retry logic below will pick it up on the next sweep.
|
|
119
|
+
await github.rest.issues.addLabels({
|
|
120
|
+
...context.repo, issue_number: issue.number,
|
|
121
|
+
labels: ['stalled', 'review'],
|
|
122
|
+
});
|
|
123
|
+
await github.rest.issues.removeLabel({ ...context.repo, issue_number: issue.number, name: 'bot-working' })
|
|
124
|
+
.catch((error) => { if (error.status !== 404) throw error; });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
77
128
|
const needsHuman = [];
|
|
78
129
|
// Gate telemetry. Every disposition the gate has ever posted is already on a pull
|
|
79
130
|
// request, marked, so the rate costs one extra listing rather than a store.
|
|
@@ -337,6 +388,7 @@ runs:
|
|
|
337
388
|
await core.summary
|
|
338
389
|
.addHeading('Housekeeping', 2)
|
|
339
390
|
.addList([
|
|
391
|
+
`Cancelled stuck pending runs: ${cancelledRuns}`,
|
|
340
392
|
`Retried: ${retried}`,
|
|
341
393
|
`Waiting on a person: ${needsHuman.length}`,
|
|
342
394
|
`Stalled, retrying by itself: ${retrying.length}`,
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -442,53 +442,6 @@ The version bump, the tag and the Release are deterministic on purpose. The agen
|
|
|
442
442
|
a file on disk, so a confused model cannot publish a release.
|
|
443
443
|
|
|
444
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
445
|
---
|
|
493
446
|
|
|
494
447
|
## housekeeping
|
|
@@ -657,3 +610,52 @@ The route matrix asserts the scanner exists, that both exit paths call `setFaile
|
|
|
657
610
|
at least as many scans as upstream writes, that each individual check is still present, and that no
|
|
658
611
|
model or `OPENAI_API_KEY` has appeared in the action. Every one of those was mutation-tested; three
|
|
659
612
|
earlier versions of them passed against a deliberately broken guard and were rewritten.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## agent-visual-verify.md
|
|
617
|
+
|
|
618
|
+
Runs after merge-gate decides to auto-merge, before the actual merge, and produces **evidence, not
|
|
619
|
+
code changes**: it writes a verification plan, drives a real browser through the app, and attaches
|
|
620
|
+
screenshots to the linked issue. It is called directly by merge-gate's `conclude` job, so it is not
|
|
621
|
+
a router route. Failures never block the merge — a browser that will not start or an app that never
|
|
622
|
+
comes up emits a warning comment and the merge proceeds.
|
|
623
|
+
|
|
624
|
+
```mermaid
|
|
625
|
+
flowchart TD
|
|
626
|
+
vvStart("merge-gate conclude<br/>calls agent-visual-verify") --> vvSubject
|
|
627
|
+
vvSubject["subject (rung 4)<br/>PR open? linked issue found?"] --> vvConfig
|
|
628
|
+
vvConfig{"config (rung 4)<br/>VISUAL_VERIFY_ENABLED and<br/>START_COMMAND set?"}
|
|
629
|
+
vvConfig -.->|"no"| vvIdle
|
|
630
|
+
vvConfig -->|"yes"| vvFacts
|
|
631
|
+
vvFacts["Facts (rung 3)<br/>PR branch checked out,<br/>issue context loaded"] --> vvPlan
|
|
632
|
+
vvPlan{"/repo-verify (rung 5)<br/>reachable UI surface?"}
|
|
633
|
+
vvPlan -.->|"not-applicable"| vvNoUi
|
|
634
|
+
vvPlan -->|"yes"| vvStartCmd
|
|
635
|
+
vvStartCmd{"start command set?<br/>VISUAL_VERIFY_START_COMMAND"}
|
|
636
|
+
vvStartCmd -.->|"empty"| vvNoStart
|
|
637
|
+
vvStartCmd -->|"yes"| vvAgent
|
|
638
|
+
vvAgent["Agent (rung 5)<br/>parse waypoints into plan.json,<br/>one add_comment on the issue"] --> vvCapture
|
|
639
|
+
vvCapture["post-agent (rung 6)<br/>build + start the app,<br/>agent-browser captures per waypoint"] --> vvAttach
|
|
640
|
+
vvAttach["attach-screenshots (rung 6)<br/>always(), subject found + enabled"] --> vvShutdown
|
|
641
|
+
vvShutdown["shut down the app"] --> vvDone
|
|
642
|
+
vvCapture -.->|"app never started,<br/>no plan, or all waypoints failed"| vvPartial
|
|
643
|
+
vvDone(("Verified<br/>screenshots attached,<br/>merge proceeds"))
|
|
644
|
+
vvPartial(("Attach what exists<br/>no-capture explanation,<br/>run stays green"))
|
|
645
|
+
vvNoUi(("No UI surface<br/>summary comment, stop"))
|
|
646
|
+
vvNoStart(("Not configured<br/>warning comment, stop"))
|
|
647
|
+
vvIdle(("Idle<br/>unconfigured or PR not open"))
|
|
648
|
+
|
|
649
|
+
classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
|
|
650
|
+
classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
|
|
651
|
+
classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
|
|
652
|
+
classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
|
|
653
|
+
classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
|
|
654
|
+
classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
|
|
655
|
+
class vvStart start
|
|
656
|
+
class vvSubject,vvFacts,vvCapture,vvAttach,vvShutdown action
|
|
657
|
+
class vvConfig,vvPlan,vvStartCmd decision
|
|
658
|
+
class vvIdle idle
|
|
659
|
+
class vvNoUi,vvNoStart,vvPartial failure
|
|
660
|
+
class vvDone success
|
|
661
|
+
```
|