Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changes/autonomy/work-resolve-issue-issue-7469-c2f85a17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Changed

- `work-classes.md` names the babysit merge gate's rungs `merge` (`C2`) and `merge-reviewed` (`C3`) where it maps each rung to its promotion cells ([#7469](https://github.com/melodic-software/claude-code-plugins/issues/7469)).
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: minor
---

### Changed

- `merge.rung` takes names for what each rung needs before the merge authority merges: `off`, `merge` and `merge-reviewed`. `C2` and `C3` stay accepted as aliases for `merge` and `merge-reviewed`, and each conditional rung requirement applies to both spellings. The example and this repository's config use `merge`, and the README names the rung first with its code in parentheses ([#7469](https://github.com/melodic-software/claude-code-plugins/issues/7469)).
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: minor
---

### Added

- The babysit merge gate reads the `merge.rung` names `merge` and `merge-reviewed` as the rungs `C2` and `C3`, at the same rank and with the same promotion cells, in the config, `CLAUDE_LANES_MERGE_RUNG_CAP` and `--lane-rung-cap`. `C2` and `C3` stay accepted as aliases, and the gate still reports the rung by its code ([#7469](https://github.com/melodic-software/claude-code-plugins/issues/7469)).
18 changes: 18 additions & 0 deletions .github/actions/resolve-config/merge-authority.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,24 @@ test("rung C2 with authority local-babysit fails on both the missing diff-check
);
});

// Each rung above off has a name and a code kept as its alias; both spellings
// carry the same conditional requirements.
for (const rung of ["merge", "merge-reviewed", "C2", "C3"]) {
test(`rung ${rung} validates and carries the requirements of a rung above off`, () => {
assert.doesNotThrow(() => run(withMerge({ rung })));
assert.doesNotThrow(() => run(withMerge({ ...LANE, rung, "require-verifier-verdict": true })));
rejectedFor(withMerge({ ...LANE, rung }), /require-verifier-verdict/);
rejectedFor(
withMerge({ rung, authority: "local-babysit", "agent-identity": IDENTITY }),
/required property 'diff-check'/,
);
});
}

test("a rung outside off, its names and its codes fails the schema", () => {
rejectedFor(withMerge({ rung: "C4" }), /\/merge\/rung must be equal to one of the allowed values/);
});

test("an agent-identity without the [bot] suffix fails the schema: it must be a GitHub App", () => {
rejectedFor(
withMerge({ authority: "local-babysit", "agent-identity": "some-user" }),
Expand Down
13 changes: 11 additions & 2 deletions docs/contracts/loop-lane/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ topology, the escalation contract, the capability-tier vocabulary, or any loop-l
major bump, and additive guidance is a minor bump. A new model release re-audits the capability-tier
table (§3); drift found by that audit is recorded here.

## [13.1.1] - 2026-11-01

Patch. Wording only; no topology, escalation-contract mechanism, capability-tier or §4 invariant
changed.

- **Rung names (§1).** The unattended merge gate's rungs are named `merge` (`C2`) and
`merge-reviewed` (`C3`) where the contract maps each rung to its promotion cells, matching the
`merge.rung` names in the PR pipeline convention (#7469).

## [13.1.0] - 2026-10-31

Minor. Guidance only: confirms an interim §8 mechanism and adds how to read it; no topology,
Expand Down Expand Up @@ -709,7 +718,7 @@ A change to the escalation contract is a major bump per this file's own rule.
- **Escalation contract (§2), escalation record write.** Every escalation an autonomous lane
files now also writes a local JSON escalation record at
`.claude/lane-escalations/<UTC-stamp>-<item>-<lane>.json`, created with the Write tool (never a
shell redirect, whose `Bash` event the seam's `Write` matcher never sees), one new file per
shell redirect, whose `Bash` event the hook's `Write` matcher never sees), one new file per
NEWLY filed escalation, since suppression is the marker read a lane already performs before
escalating, so a standing escalation re-encountered on a later cycle fires no second webhook. The
record is written **immediately before** the marker comment, and the order is part of the
Expand All @@ -730,7 +739,7 @@ A change to the escalation contract is a major bump per this file's own rule.
`PushNotification` and `slack`-plugin outbound are
named as optional model-discretionary layers, never the deterministic leg. Fan-out depth on the
one filed escalation, not a second escalation channel; degradation without a configured hook
loses only the out-of-band leg. §2 also records the seam's egress (the POST body is the full
loses only the out-of-band leg. §2 also records the hook's egress (the POST body is the full
Comment thread
kyle-sexton marked this conversation as resolved.
Resolved
hook input, session metadata included, consumer-opted by configuring the hook) and its
silent-failure mode (empty-string env interpolation plus non-blocking non-2xx), with a
wire-time verification step.
Expand Down
10 changes: 5 additions & 5 deletions docs/contracts/loop-lane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ too ([the pr-merge lane](../../conventions/pr-pipeline/README.md#the-pr-merge-la
[`work-classes.md`](../../../plugins/autonomy/reference/guardrails/work-classes.md#merge-authorities-that-read-no-class)).
The unattended merge gate (`/source-control:babysit-prs`, `--unattended`) reads no class either,
by [owner decision](https://github.com/melodic-software/claude-code-plugins/issues/9668#issuecomment-6386299330):
no class caps its rung, its promotion evidence is read by rung, not class (C2 needs
`C2-auto-merge`, C3 every C3 cell), and its one class-named
no class caps its rung, its promotion evidence is read by rung, not class (`merge` (`C2`) needs
`C2-auto-merge`, `merge-reviewed` (`C3`) every C3 cell), and its one class-named
check is the trust hold, which refuses a pull request whose linked issue carries `work-class:
untrusted-provenance` or had it removed by a non-human. Its rung and rung cap remain pacing caps.
Higher rungs are opt-in per repository, up to full autonomy, where frontier-tier subagents resolve
Expand Down Expand Up @@ -530,7 +530,7 @@ Three rules make the signal deterministic:
order fails the other way and cannot be recovered: a stop after the marker post leaves the marker
standing with no record ever written, and that standing marker suppresses the record on every
later cycle, so the out-of-band notification for that escalation is lost permanently. Ordering is
what makes the seam fail loud rather than silent; no reconciliation pass is needed, and none
what makes the notification path fail loud rather than silent; no reconciliation pass is needed, and none
would be reliable, since a compensating write can stop in exactly the same window.

**Background-job launch mode.** The tracker marker comment is the escalation of record whether or
Expand Down Expand Up @@ -592,7 +592,7 @@ is read live at the pointer.
- **Recheck trigger**: a Claude Code release note or docs change touching HTTP hook handlers, the
`if` field, header interpolation, or how edit rules anchor their paths.

- The seam is an `http` handler on `PostToolUse` in the consuming repo's project
- The notification hook is an `http` handler on `PostToolUse` in the consuming repo's project
`.claude/settings.json`: per-consuming-repo configuration that no plugin ships. We rely on it as
deterministic (it fires on the matched lifecycle event, no model judgment) and as carrying no
claude.ai subscription or Remote Control dependency.
Expand Down Expand Up @@ -638,7 +638,7 @@ Only the http hook is the deterministic leg.
path is relative to the session's checkout, and the hook that fires is the one in that session's
loaded project settings. So a lane whose scope argument names a repository other than its own
checkout, a supported merge-lane mode, POSTs to the *launching* project's endpoint, and the
target repository's tracked hook is never consulted. That is the seam as specified rather than a
target repository's tracked hook is never consulted. That is the hook working as specified rather than a
Comment thread
kyle-sexton marked this conversation as resolved.
Resolved
misconfiguration: "the consuming repo" is whichever project the lane session runs in, which is also
the project whose settings the harness loaded. **Running the lane from the target repository's own
checkout is therefore a requirement, not a preference, whenever that repository's endpoint is the
Expand Down
10 changes: 6 additions & 4 deletions docs/conventions/pr-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -981,10 +981,12 @@ branch-cleanup:
max-deletes-per-run: 25

merge:
# The lane treats every PR the lanes App opened for one closing issue as
# class C2 (plugins/autonomy/reference/guardrails/work-classes.md) and reads
# no `work-class:` label. Its merge criterion is the verifier verdict below.
rung: C2
# The rung is a pacing cap: off merges nothing without a human; merge lets
# the merge authority merge; merge-reviewed also needs AI review to be
# blocking on the local babysit lane. C2 and C3 are accepted aliases for
# merge and merge-reviewed. The lane reads no `work-class:` label: its merge
# criterion is the verifier verdict below.
rung: merge
authority: pr-merge
agent-identity: melodic-automation-lanes[bot]
# The Apps whose touch holds a trust-gate path change for a person in
Expand Down
15 changes: 10 additions & 5 deletions docs/conventions/pr-pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,10 +984,13 @@ run by the local babysit lane (loop-lane), or `pr-merge`. When it is absent the
reads the authority the wrapper runs as: `local-babysit` by default, `pr-merge` under
`--authority pr-merge` (below). `merge.report-only: true` makes that authority report each
decision and write nothing. `merge.rung` is a pacing cap: `off` merges nothing without a human,
and `C2` or `C3` lets the authority merge. Neither authority reads a work class
and `merge` or `merge-reviewed` lets the authority merge. On the pr-merge lane both merge on the
verifier verdict; on the local babysit lane `merge-reviewed` also needs AI review to be blocking
(its promotion cells, below). `C2` and `C3` are accepted aliases for `merge` and `merge-reviewed`.
Neither authority reads a work class
([`work-classes.md`](../../../plugins/autonomy/reference/guardrails/work-classes.md#merge-authorities-that-read-no-class)).

- `off` is the default. The first rung a repository adopts is `C2`, together with
- `off` is the default. The first rung a repository adopts is `merge` (`C2`), together with
`merge.diff-check`: the diff touches no denied path and stays under the size cap.
- No work class is read, and no class, missing class or diff shape caps the rung. The only
class-named check is the untrusted-provenance hold: a pull request whose linked issue carries
Expand Down Expand Up @@ -1058,7 +1061,8 @@ the rules that only tighten in
key, or either file the wrapper cannot read or trust, means `off`; a YAML without it leaves the
legacy value, and with no legacy file the rung is `off`, so the YAML alone never raises it), the
cap variable, and `off` while any demotion issue is open.
- **Cap variable.** `CLAUDE_LANES_MERGE_RUNG_CAP` (`off`, `C2` or `C3`) is a repository variable
- **Cap variable.** `CLAUDE_LANES_MERGE_RUNG_CAP` (`off`, `merge` or `merge-reviewed`, or the
aliases `C2` and `C3`) is a repository variable
only a human sets, and it can only lower the rung. The wrapper never writes it, and the identity
can read variables but not write them. A 404 after the variables listing proved readable means no
cap; any other failure means `off`.
Expand Down Expand Up @@ -1599,7 +1603,8 @@ except six:
stops the lane. The value is read once per run, and a stopped run cannot dequeue: to stop at
once, cancel the running pr-merge run and dequeue the lane's pull request by hand.
- **Cap variable.** For the same reason the workflow passes `CLAUDE_LANES_MERGE_RUNG_CAP` from its
`vars` context as `--lane-rung-cap`. Empty (unset) is no cap; `off`, `C2` or `C3` caps the rung;
`vars` context as `--lane-rung-cap`. Empty (unset) is no cap; `off`, `merge` or `merge-reviewed`
(or the aliases `C2` and `C3`) caps the rung;
a missing flag or any other value holds the rung at `off`.
- **Post-merge check and merges in flight.** Only merges the agent identity enqueued count. A
merge by a person or by another App, such as a release pull request, is not the lane's.
Expand Down Expand Up @@ -2104,7 +2109,7 @@ the same rules (`babysit_dependabot.py` here, `dependency-classify.mjs` in pr-un
always refuses, verdict or not.
- **Gate.** Either tier merges only once `ci-status` succeeded on its head, through the same gate
as the lane's own: mergeable, no `do-not-merge` label, no body hold, no blocking comment from a
human with write access, the rung (C2) and its cap, the kill switches, the post-merge check and
human with write access, the rung (`merge` (`C2`) or above) and its cap, the kill switches, the post-merge check and
the merges in flight. No closing issue, denied-path rule or diff risk score applies: `ci-status`
(and, for tier `review`, the review verdict) is the gate, and a workflow pin bump touches
`.github/**` by definition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ models:
claude: review

merge:
rung: C2
# off, merge or merge-reviewed (README.md, Merge authority).
rung: merge
# The merge authority and, for local-babysit, the login the lane acts as
# (README.md, Merge authority). local-babysit requires agent-identity.
# authority: local-babysit
Expand Down
8 changes: 4 additions & 4 deletions docs/conventions/pr-pipeline/pr-pipeline.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,7 @@
"if": {
"required": ["rung", "authority"],
"properties": {
"rung": { "enum": ["C2", "C3"] },
"rung": { "enum": ["merge", "merge-reviewed", "C2", "C3"] },
"authority": { "const": "local-babysit" }
}
},
Expand All @@ -1018,7 +1018,7 @@
"if": {
"required": ["rung", "agent-identity"],
"properties": {
"rung": { "enum": ["C2", "C3"] },
"rung": { "enum": ["merge", "merge-reviewed", "C2", "C3"] },
"authority": { "const": "pr-merge" }
}
},
Expand All @@ -1030,9 +1030,9 @@
],
"properties": {
"rung": {
"enum": ["off", "C2", "C3"],
"enum": ["off", "merge", "merge-reviewed", "C2", "C3"],
"default": "off",
"description": "Pacing cap for the merge authority: off merges nothing without a human; C2 or C3 lets it merge. No work class is read; the only class-named check is the local babysit lane's untrusted-provenance hold (README.md, Merge authority)."
"description": "Pacing cap for the merge authority: off merges nothing without a human; merge or merge-reviewed lets it merge, and on the local babysit lane merge-reviewed also needs AI review to be blocking. C2 and C3 are accepted aliases for merge and merge-reviewed. No work class is read; the only class-named check is the local babysit lane's untrusted-provenance hold (README.md, Merge authority)."
},
"authority": {
"enum": ["local-babysit", "pr-merge"],
Expand Down
4 changes: 2 additions & 2 deletions lib/parse-concern-value.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ assert_exit "no args exits 2" 2 "$rc"
# --- Dotted keys and lists, read from the pipeline's canonical example ---
# Expected values are the literals on the cited lines of that example file.
EX="$SCRIPT_DIR/../docs/conventions/pr-pipeline/examples/claude-code-plugins.yaml"
assert_eq 'nested key merge.rung (example :173)' "C2" "$(bash "$SCRIPT" "$EX" merge.rung)"
assert_eq '--list prints a flow list one item per line, quotes removed (example :179)' \
assert_eq 'nested key merge.rung (example :201)' "merge" "$(bash "$SCRIPT" "$EX" merge.rung)"
assert_eq '--list prints a flow list one item per line, quotes removed (example :207)' \
$'.github/**\n.claude/**\ndocs/conventions/pr-pipeline.yaml' \
"$(bash "$SCRIPT" "$EX" merge.diff-check.denied-paths --list)"
assert_eq 'a block sequence of mappings is indexed (example :143)' "merge" \
Expand Down
4 changes: 2 additions & 2 deletions plugins/autonomy/reference/guardrails/work-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ authority refuses every fork change request and every change request its automat
the dependency manager did not open.

The babysit unattended merge gate (`/source-control:babysit-prs`, its `--unattended` path) is
another. It reads no class; it needs its effective rung's promotion cells (rung C2 the `C2-auto-merge`
cell, rung C3 every C3 cell), read by rung, never by class. The one label it
another. It reads no class; it needs its effective rung's promotion cells (rung `merge` (`C2`) the
`C2-auto-merge` cell, rung `merge-reviewed` (`C3`) every C3 cell), read by rung, never by class. The one label it
reads by a class name is a trust hold: it refuses a change request whose linked item carries
`work-class: untrusted-provenance`, or had that label removed by a non-human, and it needs the
linked item vouched for by a human (written by one with write access, or given the admission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1081,8 +1081,8 @@ any refusal is the blocker `unattended merge refused: <reason>`:
promotion evidence (`unattended.promoted`, from the babysit-loop resolver, which reads its four
options from the operator's managed and user settings, never from arguments or the
environment), and eligibility. The cells are the effective rung's, never the pull request's
work class: rung C2 needs `C2-auto-merge`, rung C3 every C3 cell
(`promotion-unqualified:<rung>` otherwise); an unreadable or untrusted resolver refuses as
work class: rung `merge` (`C2`) needs `C2-auto-merge`, rung `merge-reviewed` (`C3`) every C3
cell (`promotion-unqualified:<rung>` otherwise, naming the rung by its code); an unreadable or untrusted resolver refuses as
`promotion-unqualified:<reason>`. A settings file, or a binding, probe root, evidence source or
checker it names, that the lane user can write, or replace through any directory above it,
refuses; every worktree of the checkout is passed to the resolver as a `--worktree-root`, so a
Expand Down
Loading
Loading