@plainconceptsplatform/workflows 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/route-processing.js +1 -0
  2. package/loops/actions/add-issue-labels/action.yml +50 -50
  3. package/loops/actions/agent-output.cjs +17 -17
  4. package/loops/actions/apply-agent-bundle/action.yml +24 -24
  5. package/loops/actions/apply-agent-comments/action.yml +42 -42
  6. package/loops/actions/apply-agent-labels/action.yml +55 -55
  7. package/loops/actions/apply-agent-output/action.yml +108 -108
  8. package/loops/actions/assess-blast-radius/action.yml +47 -8
  9. package/loops/actions/assess-blast-radius/assess-blast-radius.sh +27 -5
  10. package/loops/actions/assess-blast-radius/migration-is-additive.sh +38 -0
  11. package/loops/actions/classify-route/action.yml +2 -1
  12. package/loops/actions/classify-route/classify-route.sh +7 -1
  13. package/loops/actions/cleanup-artifacts/action.yml +91 -91
  14. package/loops/actions/close-agent-issues/action.yml +43 -43
  15. package/loops/actions/create-agent-issues/action.yml +52 -52
  16. package/loops/actions/create-issue-comment/action.yml +29 -29
  17. package/loops/actions/download-agent-output/action.yml +53 -53
  18. package/loops/actions/link-pr-to-issue/action.yml +40 -40
  19. package/loops/actions/list-open-issues/action.yml +33 -33
  20. package/loops/actions/load-issue-context/action.yml +45 -45
  21. package/loops/actions/merge-agent-pr/action.yml +49 -49
  22. package/loops/actions/push-agent-branch/action.yml +45 -45
  23. package/loops/actions/remove-issue-labels/action.yml +37 -37
  24. package/loops/actions/update-agent-issues/action.yml +58 -58
  25. package/loops/actions/validate-merge-gate-output/action.yml +6 -1
  26. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +9 -2
  27. package/loops/actions/validate-review-output/action.yml +35 -35
  28. package/loops/actions/validate-triage-output/action.yml +36 -36
  29. package/loops/actions/verify-composite-actions/action.yml +9 -9
  30. package/loops/actions/verify-refine-output/action.yml +9 -9
  31. package/loops/actions/verify-route-matrix/action.yml +9 -9
  32. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +149 -29
  33. package/loops/docs/diagrams.md +26 -11
  34. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  35. package/loops/templates/issues/bug_report.yml +109 -109
  36. package/loops/templates/issues/feature_request.yml +75 -75
  37. package/loops/templates/release/github-release.yml +30 -30
  38. package/loops/workflows/agent-apply-review.md +5 -0
  39. package/loops/workflows/agent-implement.md +6 -0
  40. package/loops/workflows/agent-merge-gate.md +100 -3
  41. package/loops/workflows/authorize-bot-work.yml +105 -105
  42. package/loops/workflows/evidence.yml +79 -4
  43. package/loops/workflows/shared/platform-defaults.md +23 -23
  44. package/loops/workflows/work-router.yml +97 -21
  45. package/package.json +1 -1
@@ -78,7 +78,7 @@ route_field() {
78
78
  unset EVENT ACTION LABEL ISSUE_LABELS EVENT_ISSUE_NUMBER EVENT_PR_NUMBER \
79
79
  COMMENT_ON_PR COMMENT_SENDER_TYPE RUN_PR_NUMBER RUN_CONCLUSION RUN_ID \
80
80
  SCHEDULE OPERATION INPUT_ISSUE_NUMBER INPUT_PR_NUMBER INPUT_MODE \
81
- INPUT_CI_CONCLUSION INPUT_CI_RUN_ID INPUT_TRIGGER_KIND PR_MERGED EVIDENCE_ENABLED
81
+ INPUT_CI_CONCLUSION INPUT_CI_RUN_ID INPUT_TRIGGER_KIND PR_MERGED EVIDENCE_ENABLED REVIEW_STATE
82
82
 
83
83
  for assignment in "${assignments[@]}"; do
84
84
  key="${assignment%%=*}"
@@ -185,6 +185,12 @@ assert_route "a review comment routes to apply-review" apply-review \
185
185
  EVENT=pull_request_review_comment EVENT_PR_NUMBER=7
186
186
  assert_route "a submitted review routes to apply-review" apply-review \
187
187
  EVENT=pull_request_review EVENT_PR_NUMBER=7
188
+ assert_route "a review requesting changes routes to apply-review" apply-review \
189
+ EVENT=pull_request_review REVIEW_STATE=changes_requested EVENT_PR_NUMBER=7
190
+ assert_route "an approval routes to merge-approved" merge-approved \
191
+ EVENT=pull_request_review REVIEW_STATE=approved EVENT_PR_NUMBER=7
192
+ assert "merge-approved carries the pull request" 7 \
193
+ "$(route_field pr-number EVENT=pull_request_review REVIEW_STATE=approved EVENT_PR_NUMBER=7)"
188
194
  assert_route "a pull_request_target routes to bot-approve" bot-approve \
189
195
  EVENT=pull_request_target ACTION=opened
190
196
  assert_route "a merged pull request routes to evidence" evidence \
@@ -704,7 +710,7 @@ if [ "$(count -c 'attempts_so_far' "$ROUTER_YML")" -lt 2 ]; then
704
710
  BELT_OK=0; echo "FAIL: dispatch sites must forward attempts_so_far" >&2
705
711
  fi
706
712
  # A second gate for a pull request whose gate is already queued or running reads the same CI
707
- # verdict and is cancelled by the single-slot merge-belt queue (two cancellations on 2026-09-06).
713
+ # verdict, and the belt's queue would run it twice.
708
714
  if [ "$(count -c 'a merge-gate run is already live' "$ROUTER_YML")" -lt 2 ]; then
709
715
  BELT_OK=0; echo "FAIL: both dispatch paths must skip a pull request whose gate is already live" >&2
710
716
  fi
@@ -1079,6 +1085,17 @@ for route in "${EXCLUDED_ROUTES[@]}"; do
1079
1085
  fi
1080
1086
  done
1081
1087
 
1088
+ # merge-approved serves the gate and leaves with it.
1089
+ if worker_installed merge-gate; then
1090
+ if grep -q "route == 'merge-approved'" "$ROUTER_YML"; then PASS=$((PASS + 1)); else
1091
+ FAIL=$((FAIL + 1)); echo "FAIL: work-router.yml has no job for route 'merge-approved'" >&2
1092
+ fi
1093
+ elif grep -q "route == 'merge-approved'" "$ROUTER_YML"; then
1094
+ FAIL=$((FAIL + 1)); echo "FAIL: merge-approved is installed without the merge gate" >&2
1095
+ else
1096
+ PASS=$((PASS + 1))
1097
+ fi
1098
+
1082
1099
  while read -r operation; do
1083
1100
  if grep -q "route == '${operation}'" "$ROUTER_YML"; then
1084
1101
  PASS=$((PASS + 1))
@@ -1415,7 +1432,93 @@ level=low")
1415
1432
  printf '%s\n' "$blast_injection" >&2
1416
1433
  fi
1417
1434
 
1435
+ # Ignored paths leave the size, never the protection: a protected path that only reaches the
1436
+ # script through ALL_PATHS still holds the merge.
1437
+ blast_ignored=$(PROTECTED_PATHS='^\.' OWNER_PATHS='' SENSITIVE_PATHS='' \
1438
+ HIGH_FILES=20 HIGH_LINES=800 MEDIUM_FILES=5 MEDIUM_LINES=200 \
1439
+ ALL_PATHS=$'src/ui/list.tsx\n.github/workflows/ci.yml' IGNORED_FILES=1 IGNORED_LINES=4 \
1440
+ bash "$BLAST_SCRIPT" 1 10 <<<"src/ui/list.tsx")
1441
+ printf '%s\n' "$blast_ignored" | grep -qx 'requires_review=true' ||
1442
+ { BLAST_OK=0; echo "FAIL: a protected path outside the counted files no longer holds the merge" >&2; }
1443
+
1444
+ # An additive migration under an owner path asks for a second look, not its owner. Any other
1445
+ # owner path in the same diff still forces high.
1446
+ blast_additive() {
1447
+ PROTECTED_PATHS='^\.' OWNER_PATHS='(^|/)([Mm]igrations|auth)/' SENSITIVE_PATHS='' \
1448
+ HIGH_FILES=20 HIGH_LINES=800 MEDIUM_FILES=5 MEDIUM_LINES=200 \
1449
+ ADDITIVE_MIGRATIONS="$1" bash "$BLAST_SCRIPT" 2 120 <<<"$2" | sed -n 's/^level=//p'
1450
+ }
1451
+ [ "$(blast_additive 'src/Migrations/1_AddX.cs' $'src/Migrations/1_AddX.cs\nsrc/ui/x.tsx')" = medium ] ||
1452
+ { BLAST_OK=0; echo "FAIL: an additive migration still forces its owner" >&2; }
1453
+ [ "$(blast_additive '' $'src/Migrations/1_AddX.cs\nsrc/ui/x.tsx')" = high ] ||
1454
+ { BLAST_OK=0; echo "FAIL: a migration nobody proved additive no longer needs its owner" >&2; }
1455
+ [ "$(blast_additive 'src/Migrations/1_AddX.cs' $'src/Migrations/1_AddX.cs\nsrc/auth/login.ts')" = high ] ||
1456
+ { BLAST_OK=0; echo "FAIL: an additive migration let another owner path through" >&2; }
1457
+
1458
+ # The shipped ignore list, against paths from real consumer pull requests.
1459
+ ignore_re=$(sed -n "s/^ BLAST_IGNORE_PATHS: '\(.*\)'$/\1/p" "$MERGE_GATE_WORKER_MD")
1460
+ for ignored in tests/Pliny.UnitTests/Agents/SecretRedactorTests.cs apps/api/tests/Numa.UnitTests/QuoteTests.cs \
1461
+ apps/web/src/views/chat/ui/internals-pane.test.tsx apps/web/src/shared/config/i18n/es.json \
1462
+ openspec/changes/archive/x/verification-plan.md docs/ARCH.md \
1463
+ src/Pliny.Infrastructure/Migrations/20261014225437_X.Designer.cs src/Migrations/AppDbContextModelSnapshot.cs; do
1464
+ grep -qE "$ignore_re" <<<"$ignored" ||
1465
+ { BLAST_OK=0; echo "FAIL: BLAST_IGNORE_PATHS counts ${ignored} toward the diff's size" >&2; }
1466
+ done
1467
+ for counted in src/Pliny.Infrastructure/Agents/SecretRedactor.cs apps/web/src/views/chat/ui/internals-pane.tsx \
1468
+ src/Pliny.Infrastructure/Migrations/20261014225437_X.cs apps/api/src/Numa.Api/appsettings.json; do
1469
+ if grep -qE "$ignore_re" <<<"$counted"; then
1470
+ BLAST_OK=0; echo "FAIL: BLAST_IGNORE_PATHS ignores ${counted}, which ships" >&2
1471
+ fi
1472
+ done
1473
+
1418
1474
  if [ "$BLAST_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1475
+
1476
+ # A migration is additive only when its Up() creates and adds. Down() is all drops by design,
1477
+ # and a data rewrite or a required column with no default needs the owner.
1478
+ MIGRATION_OK=1
1479
+ MIGRATION_SCRIPT="${HERE}/../assess-blast-radius/migration-is-additive.sh"
1480
+ migration_case() {
1481
+ local name="$1" want="$2" patch="$3" got
1482
+ if bash "$MIGRATION_SCRIPT" <<<"$patch" >/dev/null; then got=additive; else got=owner; fi
1483
+ [ "$got" = "$want" ] || { MIGRATION_OK=0; echo "FAIL: migration '${name}' read as ${got}, expected ${want}" >&2; }
1484
+ }
1485
+ migration_case "a new table, dropped in Down()" additive '+ protected override void Up(MigrationBuilder migrationBuilder)
1486
+ + {
1487
+ + migrationBuilder.CreateTable(name: "TokenRevocations", columns: table => new { Id = table.Column<Guid>(nullable: false) });
1488
+ + migrationBuilder.AddColumn<string>(
1489
+ + name: "Note",
1490
+ + table: "Quotes",
1491
+ + nullable: true);
1492
+ + }
1493
+ + protected override void Down(MigrationBuilder migrationBuilder)
1494
+ + {
1495
+ + migrationBuilder.DropTable(name: "TokenRevocations");
1496
+ + }'
1497
+ migration_case "a data rewrite" owner '+ protected override void Up(MigrationBuilder migrationBuilder)
1498
+ + {
1499
+ + migrationBuilder.Sql("UPDATE Bots SET Instructions = NULL");
1500
+ + }'
1501
+ migration_case "a renamed column" owner '+ protected override void Up(MigrationBuilder migrationBuilder)
1502
+ + {
1503
+ + migrationBuilder.RenameColumn(name: "A", table: "T", newName: "B");
1504
+ + }'
1505
+ migration_case "a required column with no default" owner '+ protected override void Up(MigrationBuilder migrationBuilder)
1506
+ + {
1507
+ + migrationBuilder.AddColumn<int>(
1508
+ + name: "Level",
1509
+ + table: "Quotes",
1510
+ + nullable: false);
1511
+ + }'
1512
+ migration_case "a required column with a default" additive '+ protected override void Up(MigrationBuilder migrationBuilder)
1513
+ + {
1514
+ + migrationBuilder.AddColumn<int>(
1515
+ + name: "Level",
1516
+ + table: "Quotes",
1517
+ + nullable: false,
1518
+ + defaultValue: 0);
1519
+ + }'
1520
+ migration_case "no Up() in the patch" owner '+// generated'
1521
+ if [ "$MIGRATION_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1419
1522
  fi
1420
1523
 
1421
1524
  # apply-review's validator compares the unresolved review threads on a pull request against the
@@ -1471,10 +1574,10 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
1471
1574
 
1472
1575
  gate_case() {
1473
1576
  local name="$1" want="$2" json="$3" conclusion="$4"
1474
- local blast="${5:-low}" protected="${6:-false}" owner="${7:-false}"
1577
+ local blast="${5:-low}" protected="${6:-false}" owner="${7:-false}" code="${8-pass}"
1475
1578
  printf '%s' "$json" > "$gate_fixture"
1476
1579
  local got
1477
- got=$(bash "$GATE_VALIDATOR" "$gate_fixture" 7 "$conclusion" "$blast" "$protected" "$owner" 0.8 2>&1)
1580
+ got=$(bash "$GATE_VALIDATOR" "$gate_fixture" 7 "$conclusion" "$blast" "$protected" "$owner" 0.8 "$code" 2>&1)
1478
1581
  if [ "$got" != "$want" ]; then
1479
1582
  VALIDATOR_OK=0
1480
1583
  echo "FAIL: the merge-gate validator called '${name}' ${got}, expected ${want}" >&2
@@ -1500,6 +1603,14 @@ if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
1500
1603
  gate_case "an owner path needs the owner" owner-review "$(gate_items "$(gate_comment assessed)")" success low false true
1501
1604
  gate_case "a non-success CI conclusion blocks" blocked "$(gate_items "$(gate_comment assessed)")" failure low
1502
1605
 
1606
+ # check-diff on the pull request head is a measured fact like CI: failing it never merges,
1607
+ # a repository without the script is not held to it, and a job that did not report is a person's call.
1608
+ gate_case "failing check-diff blocks" blocked "$(gate_items "$(gate_comment assessed)")" success low false false fail
1609
+ gate_case "no check-diff installed auto-merges" auto-merge "$(gate_items "$(gate_comment assessed)")" success low false false absent
1610
+ gate_case "check-diff that did not report needs a person" \
1611
+ human-review "$(gate_items "$(gate_comment assessed)")" success low false false ''
1612
+ gate_case "remediating check-diff is accepted" remediated "$(gate_items "$(gate_comment remediated),${gate_push}")" success low false false fail
1613
+
1503
1614
  # The agent's report decides the rest. This is the rule the whole redesign rests on: an
1504
1615
  # unverified finding is a warning whatever severity it claims, so a model cannot fail the gate
1505
1616
  # by asserting something it did not demonstrate, and cannot pass it by understating one it did.
@@ -1724,6 +1835,28 @@ else
1724
1835
  fi
1725
1836
  ev_absent 'secrets\.' "$EVIDENCE_YML" 'evidence.yml must need no secret; GITHUB_TOKEN is enough'
1726
1837
  ev_absent 'gh (issue|pr) comment.*--attach' "$EVIDENCE_YML" 'gh --attach refuses installation tokens; commit the screenshots beside their plans'
1838
+ # An unmerged pull request's preview goes to its own branch, never to the pull request's branch
1839
+ # (a commit there invalidates the gate's verdict) and never to the default branch.
1840
+ preview_step="$(sed -n '/name: Commit a preview of the screenshots/,/name: Write the evidence comment/p' "$EVIDENCE_YML")"
1841
+ grep -q "if: steps.pr.outputs.merged != 'true'" <<<"$preview_step" ||
1842
+ { EV_OK=0; echo "FAIL: the preview step must run only for an unmerged pull request" >&2; }
1843
+ grep -qF 'git push -q origin "HEAD:refs/heads/$PREVIEW"' <<<"$preview_step" ||
1844
+ { EV_OK=0; echo "FAIL: the preview must be pushed to EVIDENCE_PREVIEW_BRANCH and nowhere else" >&2; }
1845
+ ev '^ EVIDENCE_PREVIEW_BRANCH: ' "$EVIDENCE_YML" 'evidence.yml does not declare EVIDENCE_PREVIEW_BRANCH'
1846
+ fi
1847
+
1848
+ # The gate replays the plan for a person it hands the pull request to, never on auto-merge: a
1849
+ # dispatch next to the merge is the race that sank agent-visual-verify.
1850
+ if worker_installed merge-gate; then
1851
+ ev '^ EVIDENCE_BEFORE_REVIEW: ' "$MERGE_GATE_WORKER_MD" 'merge-gate does not declare EVIDENCE_BEFORE_REVIEW'
1852
+ review_dispatch="$(sed -n '/name: Replay the verification plan for the reviewer/,/inputs\[pr-number\]/p' "$MERGE_GATE_WORKER_MD")"
1853
+ if ! grep -q '"human-review","owner-review"' <<<"$review_dispatch" || grep -q 'auto-merge' <<<"$review_dispatch" ||
1854
+ ! grep -q 'inputs\[operation\]=evidence' <<<"$review_dispatch"; then
1855
+ EV_OK=0
1856
+ echo "FAIL: the gate must dispatch evidence for human-review and owner-review only" >&2
1857
+ fi
1858
+ grep -q 'name: Replay the verification plan for the owner' "$MERGE_GATE_WORKER_MD" ||
1859
+ { EV_OK=0; echo "FAIL: a protected-path hold never reaches conclude, so it must dispatch evidence itself" >&2; }
1727
1860
  fi
1728
1861
 
1729
1862
  # The retired worker stays retired: nothing dispatches it and nothing waits for it.
@@ -2366,31 +2499,18 @@ for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
2366
2499
  done
2367
2500
  if [ "$INPUT_WIRING_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
2368
2501
 
2369
- # The belt dispatches one gate per tick and stops. The gate's concurrency group holds a single
2370
- # pending run, so GitHub cancels every earlier pending dispatch in it -- `cancel-in-progress:
2371
- # false` only protects a run that has already started. A loop without this `break` dispatched one
2372
- # gate per eligible pull request, ran the last, and discarded the rest with no comment, no
2373
- # recorded attempt, and nothing on the pull request to show it had been skipped. Asserted because
2374
- # removing the break produces no error anywhere: the extra dispatches all return 204.
2375
- BELT_ONE_PER_TICK_OK=1
2376
- belt_dispatch_block=$(awk '
2377
- /Dispatching Merge Gate for PR/ { found = 1 }
2378
- found { print }
2379
- found && /^ *done$/ { exit }
2380
- ' "$ROUTER_YML")
2381
- if [ -z "$belt_dispatch_block" ]; then
2382
- BELT_ONE_PER_TICK_OK=0
2383
- echo "FAIL: the merge belt's gate dispatch could not be located, so its one-per-tick guard cannot be checked" >&2
2384
- elif ! printf '%s\n' "$belt_dispatch_block" | grep -qE '^ *break$'; then
2385
- BELT_ONE_PER_TICK_OK=0
2386
- echo "FAIL: the merge belt dispatches a gate per eligible pull request and never breaks; only the last stays pending and the rest are cancelled in silence" >&2
2387
- fi
2388
- # And the group it relies on must still be the serialising one.
2389
- # Anchored to the whole line: a substring search matched `merge-belt-renamed` and stayed green
2390
- # through a mutation that removed the serialisation the break depends on.
2391
- grep -qE '^ *group: merge-belt *$' "$ROUTER_YML" ||
2392
- { BELT_ONE_PER_TICK_OK=0; echo "FAIL: the merge gate is no longer serialised on the merge-belt concurrency group" >&2; }
2393
- if [ "$BELT_ONE_PER_TICK_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
2502
+ # Several paths dispatch gates at once (the reconcile tick, CI's dispatch-merge-gate, the
2503
+ # conflict rescan). Without `queue: max` GitHub keeps one pending run in the group and cancels the
2504
+ # rest with no comment and no recorded attempt; every dispatch still returns 204.
2505
+ BELT_QUEUE_OK=1
2506
+ belt_caller=$(awk '/^ call-merge-gate:/ { found = 1; print; next } found && /^ [a-z]/ { exit } found { print }' "$ROUTER_YML")
2507
+ printf '%s
2508
+ ' "$belt_caller" | grep -qE '^ *group: merge-belt *$' ||
2509
+ { BELT_QUEUE_OK=0; echo "FAIL: the merge gate is no longer serialised on the merge-belt concurrency group" >&2; }
2510
+ printf '%s
2511
+ ' "$belt_caller" | grep -qE '^ *queue: max *$' ||
2512
+ { BELT_QUEUE_OK=0; echo "FAIL: call-merge-gate must queue its waiting gates (queue: max); a single-slot group cancels every gate but the last" >&2; }
2513
+ if [ "$BELT_QUEUE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
2394
2514
 
2395
2515
  # The specific wiring that broke, asserted end to end: the router resolves the run ID, the gate
2396
2516
  # forwards it, and the action seeds from it rather than only from its own lookup.
@@ -282,7 +282,7 @@ flowchart TD
282
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
283
  gatePush(("Pushed<br/>CI runs again, gate re-enters"))
284
284
  gateFix -.->|"cannot fix"| gateHuman
285
- gateProtected{"protected_changes (rung 4b)<br/>blast radius from paths,<br/>CODEOWNERS and diff shape"}
285
+ gateProtected{"protected_changes (rung 4b)<br/>blast radius from paths, CODEOWNERS<br/>and the size of the code that ships"}
286
286
  gateProtected -.->|"protected path"| gateOwner
287
287
  gateProtected -->|"measured low, medium or high"| gateAssess
288
288
  gateAssess("Agent (rung 5)<br/>find defects, verify them,<br/>rate recoverability") --> gateDisposition
@@ -294,6 +294,10 @@ flowchart TD
294
294
  gateMerge(("AUTO-MERGE<br/>squash, issue closed,<br/>pr-pending removed"))
295
295
  gateHuman(("HUMAN REVIEW<br/>review label,<br/>implement label kept"))
296
296
  gateOwner(("OWNER REVIEW<br/>review + owner-review,<br/>CODEOWNERS asked when named"))
297
+ gateHuman --> gatePreview
298
+ gateOwner --> gatePreview
299
+ gatePreview("operation=evidence<br/>screenshots of the head<br/>before the person decides") --> gateApproved
300
+ gateApproved(("Approved by a writer<br/>merge-approved merges<br/>the approved commit"))
297
301
  gateBlocked(("BLOCKED<br/>review + blocked,<br/>the belt does not retry"))
298
302
  gateIdle(("Idle<br/>not ours, or not open"))
299
303
  gateFix -.->|"attempt 6 of 6"| gateBlocked
@@ -305,11 +309,11 @@ flowchart TD
305
309
  classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
306
310
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
307
311
  class gateStart start
308
- class gateBranch,gateFacts,gateFix,gateAssess action
312
+ class gateBranch,gateFacts,gateFix,gateAssess,gatePreview action
309
313
  class gateSubject,gateCi,gateProtected,gateDisposition decision
310
314
  class gateIdle idle
311
315
  class gateHuman,gateOwner,gateBlocked failure
312
- class gateMerge,gatePush success
316
+ class gateMerge,gatePush,gateApproved success
313
317
  ```
314
318
 
315
319
  A protected-path match holds the merge but does not stop the repair path: the agent may still fix
@@ -321,6 +325,12 @@ agent ran. A category the diff touches is no longer, on its own, a reason to par
321
325
  only a verified high or critical finding, a sensitive path, or a change that would be hard to undo
322
326
  sends it to a person.
323
327
 
328
+ Size counts only code that ships: tests, docs, specs, translations and generated migration code
329
+ (`BLAST_IGNORE_PATHS`) are left out, and a new EF Core migration whose `Up()` only creates and adds
330
+ is a medium change rather than an owner one. When the gate parks a pull request for a person it
331
+ dispatches `operation=evidence`, so the screenshots arrive before the decision, and an approval
332
+ from anyone with write access merges the approved commit (`merge-approved` in the router).
333
+
324
334
  ---
325
335
 
326
336
  ## agent-apply-review.md
@@ -628,14 +638,16 @@ Archives what a merged pull request looks like. Not an agent: the plan was writt
628
638
  implemented the change (`pc-repo-verify`), and this replays it. The router calls it when a pull
629
639
  request merges, and `operation=evidence` runs it by hand. Nothing waits on it. The screenshots are
630
640
  committed beside each plan, in an `evidence/` folder on the default branch, so the record stays with
631
- the change it proves; the comment embeds a manifest that Project Health reads. A pull request that
632
- is not merged has no plan on the default branch, so its screenshots stay in the run's artifact.
641
+ the change it proves; the comment embeds a manifest that Project Health reads. The merge gate also
642
+ runs it when it hands a pull request to a person: that pull request is not merged, so its
643
+ screenshots go to a preview folder (`pr-<number>/`) on the `evidence-previews` branch, and the
644
+ merge replaces them with the archived ones.
633
645
  The capture job runs the pull request's code with a read-only token; only the publish job writes,
634
646
  and it runs no repository code.
635
647
 
636
648
  ```mermaid
637
649
  flowchart TD
638
- evStart("router: pull request merged<br/>or operation=evidence") --> evPlans
650
+ evStart("router: pull request merged,<br/>the gate parked it for a person,<br/>or operation=evidence") --> evPlans
639
651
  evPlans{"verification-plan.md<br/>in the pull request?"}
640
652
  evPlans -.->|"no"| evIdle
641
653
  evPlans -->|"yes"| evInspect
@@ -648,8 +660,11 @@ flowchart TD
648
660
  evUp -.->|"no, after the timeout"| evUnreached
649
661
  evReplay["replay each waypoint<br/>in headless Chromium,<br/>one JPEG per waypoint"] --> evPublish
650
662
  evUnreached["record why nothing<br/>was captured"] --> evPublish
651
- evPublish["publish (GITHUB_TOKEN)<br/>commit beside each plan,<br/>evidence/ on the default branch,<br/>upsert one comment"] --> evDone
652
- evDone(("Archived<br/>beside the plan"))
663
+ evPublish{"publish (GITHUB_TOKEN)<br/>merged?"}
664
+ evPublish -->|"yes"| evDone
665
+ evPublish -->|"not yet"| evPreview
666
+ evDone(("Archived beside the plan<br/>on the default branch,<br/>one comment upserted"))
667
+ evPreview(("Preview on evidence-previews<br/>pr-number folder,<br/>same comment"))
653
668
  evIdle(("Nothing to capture"))
654
669
 
655
670
  classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
@@ -658,8 +673,8 @@ flowchart TD
658
673
  classDef idle fill:#f4f4f6,stroke:#5b5b66,stroke-width:2px,color:#2b2b33
659
674
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
660
675
  class evStart start
661
- class evApp,evReplay,evUnreached,evPublish action
662
- class evPlans,evInspect,evUp decision
676
+ class evApp,evReplay,evUnreached action
677
+ class evPlans,evInspect,evUp,evPublish decision
663
678
  class evIdle idle
664
- class evDone success
679
+ class evDone,evPreview success
665
680
  ```
@@ -1,121 +1,121 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-maintenance.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
- # Derived from the gh-aw v0.87.5 maintenance workflow.
3
- # Copy provenance is documented in docs/ownership.md.
4
- name: Agentic Maintenance
5
-
6
- on:
7
- schedule:
8
- - cron: "37 0 * * *"
9
- workflow_dispatch:
10
- inputs:
11
- operation:
12
- description: Optional maintenance operation to run
13
- required: false
14
- type: choice
15
- default: ""
16
- options:
17
- - ""
18
- - disable
19
- - enable
20
- - update
21
- - upgrade
22
- - safe_outputs
23
- - create_labels
24
- - activity_report
25
- - close_agentic_workflows_issues
26
- - clean_cache_memories
27
- - update_pull_request_branches
28
- - validate
29
- - forecast
30
- run_url:
31
- description: Run URL or run ID required for safe_outputs
32
- required: false
33
- type: string
34
- default: ""
35
- workflow_call:
36
- inputs:
37
- operation:
38
- required: false
39
- type: string
40
- default: ""
41
- run_url:
42
- required: false
43
- type: string
44
- default: ""
45
-
46
- permissions: {}
47
-
48
- jobs:
49
- close-expired-discussions:
50
- if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
51
- runs-on: agents-arc
52
- permissions:
53
- discussions: write
54
- steps:
55
- - name: Setup Scripts
56
- uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
57
- with:
58
- destination: ${{ runner.temp }}/gh-aw/actions
59
- - name: Close expired discussions
60
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
61
- with:
62
- script: |
63
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
64
- setupGlobals(core, github, context, exec, io, getOctokit);
65
- const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_discussions.cjs');
66
- await main();
67
- close-expired-issues:
68
- if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
69
- runs-on: agents-arc
70
- permissions:
71
- issues: write
72
- steps:
73
- - name: Setup Scripts
74
- uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
75
- with:
76
- destination: ${{ runner.temp }}/gh-aw/actions
77
- - name: Close expired issues
78
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
79
- with:
80
- script: |
81
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
82
- setupGlobals(core, github, context, exec, io, getOctokit);
83
- const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_issues.cjs');
84
- await main();
85
- close-expired-pull-requests:
86
- if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
87
- runs-on: agents-arc
88
- permissions:
89
- pull-requests: write
90
- steps:
91
- - name: Setup Scripts
92
- uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
93
- with:
94
- destination: ${{ runner.temp }}/gh-aw/actions
95
- - name: Close expired pull requests
96
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
97
- with:
98
- script: |
99
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
100
- setupGlobals(core, github, context, exec, io, getOctokit);
101
- const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_pull_requests.cjs');
102
- await main();
103
- cleanup-cache-memory:
104
- if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }}
105
- runs-on: agents-arc
106
- permissions:
107
- actions: write
108
- steps:
109
- - name: Setup Scripts
110
- uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
111
- with:
112
- destination: ${{ runner.temp }}/gh-aw/actions
113
- - name: Cleanup outdated cache-memory entries
114
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
115
- with:
116
- script: |
117
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
118
- setupGlobals(core, github, context, exec, io, getOctokit);
119
- const { main } = require('${{ runner.temp }}/gh-aw/actions/cleanup_cache_memory.cjs');
120
- await main();
121
- #
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-maintenance.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ # Derived from the gh-aw v0.87.5 maintenance workflow.
3
+ # Copy provenance is documented in docs/ownership.md.
4
+ name: Agentic Maintenance
5
+
6
+ on:
7
+ schedule:
8
+ - cron: "37 0 * * *"
9
+ workflow_dispatch:
10
+ inputs:
11
+ operation:
12
+ description: Optional maintenance operation to run
13
+ required: false
14
+ type: choice
15
+ default: ""
16
+ options:
17
+ - ""
18
+ - disable
19
+ - enable
20
+ - update
21
+ - upgrade
22
+ - safe_outputs
23
+ - create_labels
24
+ - activity_report
25
+ - close_agentic_workflows_issues
26
+ - clean_cache_memories
27
+ - update_pull_request_branches
28
+ - validate
29
+ - forecast
30
+ run_url:
31
+ description: Run URL or run ID required for safe_outputs
32
+ required: false
33
+ type: string
34
+ default: ""
35
+ workflow_call:
36
+ inputs:
37
+ operation:
38
+ required: false
39
+ type: string
40
+ default: ""
41
+ run_url:
42
+ required: false
43
+ type: string
44
+ default: ""
45
+
46
+ permissions: {}
47
+
48
+ jobs:
49
+ close-expired-discussions:
50
+ if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
51
+ runs-on: agents-arc
52
+ permissions:
53
+ discussions: write
54
+ steps:
55
+ - name: Setup Scripts
56
+ uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
57
+ with:
58
+ destination: ${{ runner.temp }}/gh-aw/actions
59
+ - name: Close expired discussions
60
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
61
+ with:
62
+ script: |
63
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
64
+ setupGlobals(core, github, context, exec, io, getOctokit);
65
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_discussions.cjs');
66
+ await main();
67
+ close-expired-issues:
68
+ if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
69
+ runs-on: agents-arc
70
+ permissions:
71
+ issues: write
72
+ steps:
73
+ - name: Setup Scripts
74
+ uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
75
+ with:
76
+ destination: ${{ runner.temp }}/gh-aw/actions
77
+ - name: Close expired issues
78
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
79
+ with:
80
+ script: |
81
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
82
+ setupGlobals(core, github, context, exec, io, getOctokit);
83
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_issues.cjs');
84
+ await main();
85
+ close-expired-pull-requests:
86
+ if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
87
+ runs-on: agents-arc
88
+ permissions:
89
+ pull-requests: write
90
+ steps:
91
+ - name: Setup Scripts
92
+ uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
93
+ with:
94
+ destination: ${{ runner.temp }}/gh-aw/actions
95
+ - name: Close expired pull requests
96
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
97
+ with:
98
+ script: |
99
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
100
+ setupGlobals(core, github, context, exec, io, getOctokit);
101
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_pull_requests.cjs');
102
+ await main();
103
+ cleanup-cache-memory:
104
+ if: ${{ !(github.event.repository.fork) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }}
105
+ runs-on: agents-arc
106
+ permissions:
107
+ actions: write
108
+ steps:
109
+ - name: Setup Scripts
110
+ uses: github/gh-aw-actions/setup@2a78d04403fdc6907d0f05327cffac9dbad5312d # v0.87.5
111
+ with:
112
+ destination: ${{ runner.temp }}/gh-aw/actions
113
+ - name: Cleanup outdated cache-memory entries
114
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
115
+ with:
116
+ script: |
117
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
118
+ setupGlobals(core, github, context, exec, io, getOctokit);
119
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/cleanup_cache_memory.cjs');
120
+ await main();
121
+ #