Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
68a3a82
feat(source-control): add a promotion-evidence publisher in report mode
kyle-sexton Oct 13, 2026
82d4562
fix(source-control): class merged PRs by their linked issues' labels
kyle-sexton Oct 13, 2026
a550d87
fix(source-control): close the publisher's evidence gaps from the sec…
kyle-sexton Oct 13, 2026
b1d1a52
fix(source-control): stop post-merge edits from hiding an autonomous …
kyle-sexton Oct 13, 2026
4a8f38b
fix(source-control): read only the named post-merge gate as gate evid…
kyle-sexton Oct 13, 2026
05f3ce2
fix(source-control): read post-merge full and canary runs on later SHAs
kyle-sexton Oct 13, 2026
cbf2239
fix(source-control): compare per merge and pass a merge only when ver…
kyle-sexton Oct 13, 2026
a5ef5ca
fix(source-control): degrade lane-written references instead of refusing
kyle-sexton Oct 13, 2026
ddf4d72
fix(source-control): mirror the dispatch deadline and pin read-back
kyle-sexton Oct 13, 2026
48df018
docs(source-control): record the actions:write decision as open and t…
kyle-sexton Oct 13, 2026
fd50e31
fix(source-control): clear lint-repo failures on the evidence publisher
kyle-sexton Oct 13, 2026
315b317
docs(source-control): point at GitHub's Actions permissions instead o…
kyle-sexton Oct 13, 2026
4cb4dde
fix(source-control): match lane identities exactly and restore events…
kyle-sexton Oct 13, 2026
3585c71
docs(source-control): align the publisher fragment with the deploymen…
kyle-sexton Oct 13, 2026
27a5eeb
fix(source-control): count a revert only when a human merged it
kyle-sexton Oct 13, 2026
07af269
fix(source-control): treat an unresolvable revert PR as a direct push
kyle-sexton Oct 13, 2026
e93f111
Merge remote-tracking branch 'origin/main' into feat/4588-promotion-e…
kyle-sexton Oct 13, 2026
3855bcf
fix(source-control): count every revert, whoever merged it
kyle-sexton Oct 13, 2026
0057c55
fix(source-control): name the revert term for every revert
kyle-sexton Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: minor
---

### Added

- `babysit-loop` ships `scripts/publish-promotion-evidence.mjs`, an operator-side publisher for the promotion-evidence events file (#4588). It reads GitHub only, through read-only `gh api` calls, and emits the `{cell, event, at}` contrary events the security-binding checker reads (`reverted-merge`, `gate-failure`, and `verification-divergence` for a missed-blocking finding a human confirmed) plus a C3 promotion-evidence report: each promotable cell's predicate terms with counts, window and pass or fail. Report mode, the default, writes nothing. `--out <dir>` writes `evidence.json` and an `evidence.meta.json` sidecar (`generated_at`, `through`, `sha256`, `prev_sha256`) by temp file and rename, puts the previous events file back when the sidecar rename fails, and refuses a set that drops a previously published event or an existing file that does not match its sidecar. Work classes are read from the labels of the PR and of the issues it linked at merge, as of the merge. A merge counts as autonomous when no human enqueued it or a `--lane-login` identity merged it. An autonomous merge with no readable class is listed as unclassed and its contrary events count against every cell. Only the post-merge gate counts toward `gate-failure`. That gate is the full and canary workflow runs on any later commit containing the merge, read as `babysit_postmerge.py` reads them, plus the check runs and statuses named by the required `--gate-check` on the merge commit. A cancelled run counts unless a later run of that check succeeded, and a red full run covering several merges counts against each. As in `babysit_postmerge.py`, a merge with no lane dispatch run within 6 hours, or a lane-enqueued merge whose pin comment is missing, edited or names another head, is a gate failure. Where it runs and under which identity is the operator's deployment choice (an App lane is passed as `--lane-login name[bot]`), and the resolver does not read the sidecar; merge authority is unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,49 @@ Agent-unwritable, because an agent that can write the file can delete a `reverte
keep its cell promoted. Outside the target repository's blast radius, for the same reason as the
binding.

#### The publisher

[`scripts/publish-promotion-evidence.mjs`](../scripts/publish-promotion-evidence.mjs) builds the
events file from GitHub alone, through read-only `gh api` calls: merged pull requests classed by
the `work-class:` labels their linked issues and the pull request carried at merge time, which of
them merged autonomously (no human enqueued them, or a `--lane-login` identity merged them),
reverts of those merges, the post-merge gate (the full and canary workflow runs on any later
commit that contains the merge, the dispatch deadline and the pin read-back, read as
`babysit-prs/scripts/babysit_postmerge.py` reads them, and the checks named by the required
`--gate-check` on the merge commit), and the missed-blocking label a human applied. An autonomous
merge whose class cannot be read is listed as unclassed, and its contrary events count against
every promotable cell. Its header comment states what counts as each event, including the
judgment mappings and how to set `--lane-login` for merge-queue merges.

- **Report mode** (no `--out`) prints the events array and the C3 promotion-evidence report to
stdout and writes nothing. The report gives each promotable cell's predicate terms from
[work-classes.md](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/plugins/autonomy/reference/guardrails/work-classes.md)
"Suggested default predicates" with counts, window and pass or fail, and lists merged reverts
it could not match to a merge.
- **Deployment defaults:** `--missed-blocking-label ai-review: missed-blocking` (the script's
default), and `--ai-reviewer claude[bot] --ai-reviewer chatgpt-codex-connector[bot]`, the REST
spellings of the review bots on this repository's pull requests (read from #7361's reviews,
2026-10-13).
- **Every revert counts** as `reverted-merge`, whoever merged or enqueued the revert pull request
(a human, a bot or the lane), and so does a revert commit pushed with no pull request. The
trade-off: a bot able to merge reverts can hold a cell down. That fails safe, because a revert
can only demote.
- **`--out <dir>`** writes `evidence.json` and the sidecar `evidence.meta.json`
(`generated_at`, `through`, `sha256` of the events file, `prev_sha256`), each by a temp file
and a rename. It refuses to publish when the previous events file does not match its sidecar's
`sha256`, or when the new events drop one the previous file held.

Run it as the operator, never from the lane, and point `--out` at a directory the lane cannot
write. Which host runs it, under which identity and on what schedule is the operator's
deployment choice. The resolver does not read the sidecar.

The post-merge check dispatches its full run under the lane identity. Where the permission that
dispatch needs also lets that identity delete a red run before the publisher reads it, either the
dispatch moves to an identity the lane cannot use, or a deleted red run stays a named residual
risk. Which permission covers dispatch and which covers run deletion: GitHub's
[Actions repository permissions table](https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps#repository-permissions-for-actions)
(as of 2026-10-13; recheck when GitHub adds or splits an Actions permission).

### `promotion_evidence_checker`

The `check-security-binding.mjs` the helper runs (the autonomy plugin's
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
{
"pullListItem": {
"base": {
"ref": "main"
},
"body": "Refs: #7339",
"labels": [],
"created_at": "2026-10-13T00:54:35Z",
"merge_commit_sha": "aad0b2b69bc22154c61b0857307a183d0c7821a8",
"merged_at": "2026-10-13T02:42:17Z",
"number": 7352,
"title": "fix(source-control): settle unattended merge residuals before Phase 15",
"updated_at": "2026-10-13T02:42:19Z",
"user": {
"login": "kyle-sexton",
"type": "User"
}
},
"checkRun": {
"completed_at": "2026-10-13T02:38:54Z",
"conclusion": "success",
"name": "lint-repo",
"status": "completed"
},
"status": {
"context": "ci-lanes",
"state": "success",
"created_at": "2026-10-13T02:42:00Z",
"updated_at": "2026-10-13T02:42:00Z"
},
"commit": {
"commit": {
"committer": {
"date": "2026-10-13T02:35:38Z"
},
"message": "fix(source-control): settle unattended merge residuals before Phase 15 (#7352)"
},
"sha": "aad0b2b69bc22154c61b0857307a183d0c7821a8"
},
"issueEvent": {
"actor": {
"login": "kyle-sexton",
"type": "User"
},
"created_at": "2026-10-13T01:37:37Z",
"event": "ready_for_review",
"label": null
},
"review": {
"state": "COMMENTED",
"user": {
"login": "chatgpt-codex-connector[bot]",
"type": "Bot"
}
},
"pullGraph": {
"data": {
"repository": {
"pullRequest": {
"number": 7063,
"headRefOid": "b6888664c45a19abc91c9cd819770406a6e7616d",
"mergedAt": "2026-10-13T01:47:44Z",
"lastEditedAt": "2026-10-13T01:38:02Z",
"mergeCommit": {
"oid": "6fa4f210dd0eb904a9ca1504344d8b66294ee6f5"
},
"mergedBy": {
"__typename": "User",
"login": "kyle-sexton"
},
"labels": {
"pageInfo": {
"hasNextPage": false
},
"nodes": []
},
"closingIssuesReferences": {
"pageInfo": {
"hasNextPage": false
},
"nodes": [
{
"number": 6007,
"labels": {
"pageInfo": {
"hasNextPage": false
},
"nodes": [
{
"name": "priority: medium"
},
{
"name": "status: ready"
},
{
"name": "needs-human"
},
{
"name": "work-class: scoped"
}
]
}
}
]
},
"timelineItems": {
"pageInfo": {
"hasNextPage": false
},
"nodes": [
{
"__typename": "AddedToMergeQueueEvent",
"createdAt": "2026-10-13T01:42:44Z",
"actor": {
"__typename": "User",
"login": "kyle-sexton"
},
"enqueuer": {
"__typename": "User",
"login": "kyle-sexton"
}
}
]
}
}
}
}
},
"issue": {
"labels": [
{
"name": "priority: medium"
},
{
"name": "status: ready"
},
{
"name": "needs-human"
},
{
"name": "work-class: scoped"
}
],
"number": 6007
},
"repo": {
"default_branch": "main"
},
"commentsPage": {
"data": {
"repository": {
"pullRequest": {
"comments": {
"pageInfo": {
"hasNextPage": true,
"endCursor": "Y3Vyc29yOnYyOpHPAAAAAWv4HUQ="
},
"nodes": [
{
"author": {
"__typename": "User",
"login": "kyle-sexton"
},
"body": "`ci-status` is red because this PR is a draft, not because of the change.",
"lastEditedAt": null
}
]
}
}
}
}
},
"connectedEventFromCliCli14616": {
"__typename": "ConnectedEvent",
"createdAt": "2026-10-07T08:24:54Z",
"source": {
"__typename": "PullRequest",
"number": 14616
},
"subject": {
"__typename": "Issue",
"number": 14614
}
},
"commitNotFound": {
"stdout": "{\"message\":\"No commit found for SHA: 0123456789abcdef0123456789abcdef01234567\",\"documentation_url\":\"https://docs.github.com/rest/commits/commits#list-pull-requests-associated-with-a-commit\",\"status\":\"422\"}",
"stderr": "gh: No commit found for SHA: 0123456789abcdef0123456789abcdef01234567 (HTTP 422)\n"
},
"repoNotFound": {
"data": {
"repository": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"repository"
],
"locations": [
{
"line": 1,
"column": 2
}
],
"message": "Could not resolve to a Repository with the name 'melodic-software/no-such-repo-xyz'."
}
]
},
"pullNotFound": {
"data": {
"repository": {
"pullRequest": null
}
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"repository",
"pullRequest"
],
"locations": [
{
"line": 1,
"column": 66
}
],
"message": "Could not resolve to a PullRequest with the number of 999999."
}
]
},
"workflowRun": {
"actor": {
"login": "github-merge-queue[bot]"
},
"conclusion": "failure",
"created_at": "2026-10-12T16:32:14Z",
"event": "schedule",
"head_branch": "main",
"head_sha": "2dc6ef1cef19870b4f15ba796f970980bcb48cf8",
"html_url": "https://github.com/melodic-software/claude-code-plugins/actions/runs/38278079868",
"id": 38278079868,
"run_attempt": 1,
"status": "completed",
"triggering_actor": {
"login": "github-merge-queue[bot]"
},
"updated_at": "2026-10-12T16:40:40Z"
},
"compare": {
"ahead_by": 1,
"commits": [
{
"sha": "aad0b2b69bc22154c61b0857307a183d0c7821a8"
}
],
"status": "ahead",
"total_commits": 1
}
}
Loading
Loading