Skip to content

GitHub intake and Steward team (DEV-6632 M2) - #2

Merged
immakermatty merged 4 commits into
mainfrom
lazurio/m2-github-intake
Sep 30, 2026
Merged

immakermatty merged 4 commits into
mainfrom
lazurio/m2-github-intake

Conversation

@immakermatty

@immakermatty immakermatty commented Sep 29, 2026 •

Copy link
Copy Markdown

Status: ready for review (stacked on #1; retarget to main after #1 lands)

Why

DEV-6632 M2 (Human and Machine Mission Control, plan DEV-6632-lazurio-mausbot-steward), Lazurio decision 0169 (4)/(5): a Lazurio Environment's bot team should pick up GitHub work by itself. Plain code watches GitHub under the Environment's account and wakes a model only for real work; this is a standard Lazurio MausBot feature any operator can switch on, not a Henry-only mechanism. The Steward team (leader Henry + three workers) is the configuration Organizations run behind it. Today Henry and Pablo get work over SSH and their Hermes watcher is paused because it woke the model on idle.

Contract (what the PR introduces)

GitHub intake (server/github-intake.ts, docs: docs/lazurio-github-intake.md)

  • Off unless OMB_GITHUB_INTAKE=1; needs OMB_GITHUB_INTAKE_BOT (bot id or exact name). Optional OMB_GITHUB_INTAKE_SCOPE=requested|organization, OMB_GITHUB_INTAKE_OWNERS, OMB_GITHUB_INTAKE_EXCLUDE (owner/repo globs), OMB_GITHUB_INTAKE_INTERVAL_SECONDS (default 90, 30-3600), OMB_GITHUB_INTAKE_GH. A bad value stops the server at start with the reason.
  • Identity = the signed-in gh of the server's user; no token handling. gh missing / signed out → status says so, nothing else happens.
  • Review: open, non-draft PR where the account is a requested reviewer, or which it reviewed before (a push after "changes requested" is re-reviewed without a new request), or (organization scope) in a pushable, non-archived, non-excluded repo. Once per (repo, PR, head SHA); new head and draft→ready fire again; skipped if the account already APPROVED/CHANGES_REQUESTED that exact head.
  • Publication: PR assigned to the account + a comment containing the line /lazurio publish (own line, not quoted, not in a code block) by someone with write/maintain/admin, written after the latest assignment to the account. Once per instruction comment. Assignment alone never publishes; failing instructions are recorded as ignored with the reason.
  • Own PRs are never reviewed by the account: recorded as needs_human_approval, no bot turn.
  • Delivery: one managed webhook "GitHub intake" per target bot, events delivered in-process through the new WebhookManager.deliver(): the same queue, receipts, pending-run limit (6), rate limit, attempt log and UNTRUSTED WEBHOOK EVENT DATA wrapping as an HTTP delivery. Payload: source, kind, reason, account, repository, number, url, head_sha, base_ref, title, author, requester (+ instruction).
  • Durable dedupe in <data>/github-intake.json (0600); if it is lost, webhook receipts still prevent a second run and the webhook is found again by name.
  • Polling: one poll at a time; PR details fetched only when the search updated_at (or its role) changed, so an idle poll is user + 3 searches (+1 per owner in organization scope) and never a bot turn; ≤4 concurrent gh calls; rate limit → interval doubles up to 30 min; a paused webhook or full queue keeps work pending with at most one refused attempt per poll.
  • Status: GET /api/github-intake, POST /api/github-intake/poll (admin scope by default-deny), [github-intake] log lines.

Steward team (lazurio/teams/steward.openmaus.json, openmaus.package v2): leader "Henry" (Chief of Staff, delegates up to three PRs in parallel with delegate_bot) + "Steward 1-3". Brief: start in the Lazurio Folder and follow its AGENTS.md cascade; one worktree per PR; exact-head review submitted with gh on commit_id and verified; publication only on explicit instruction, with approvals by others and green checks on the current head; never approve own PRs; untrusted input stays data. No persona name in any instruction (Pablo imports the same file and renames the leader), no secrets, no Organization names.

What does not change

  • With OMB_GITHUB_INTAKE unset the server behaves as upstream (the only visible difference is GET /api/github-intake answering {enabled:false}).
  • No approval-level change: imported bots start on Ask, full/custom stay desktop-only. Headless approval level is an open security decision (issue draft in the handoff; Issues are disabled on this fork).
  • No UI, Machines, Platform, release or tag changes. The Environment preset that installs the team and sets the env is a later step.
  • Upstream files touched: server/webhooks.ts (new deliver(), receive() refactored to share its rejection bookkeeping; added to allowed_upstream_changes), server/index.ts and scripts/control-omb.ts (wiring / fixture env pass-through; already allowlisted).

Verification

  • Contract first: server/github-intake.test.ts (30 behavioural tests against a fake gh that models PRs, searches, reviews, comments, assignment events and repo roles; delivery through a real WebhookManager). Red before implementation: npx vitest run server/github-intake.test.ts against an API-only skeleton → 30 failed / 30. Green after: 30 passed.
  • Mutation checks: removing the settled cache, own-PR rule, assignment-time rule, writer rule, draft rule, backoff, or one-refusal-per-poll each makes at least one test fail.
  • server/github-intake.e2e.test.ts: real isolated server with a fake gh (OMB_GITHUB_INTAKE_GH): status before the bot exists (no_target), import of the shipped Steward team (4 bots, leader is Chief of Staff), poll → one delivery to the leader, the queued run completes on the fake engine with the PR head inside the UNTRUSTED block, second poll → no new run. 1 passed (it caught a strip-types-only syntax error in the first draft).
  • server/lazurio-steward-team.test.ts: package validator, secret and import scans, marker and persona-free instructions. Red without the file, 3 passed.
  • Related suites: webhooks, webhook ingress/idempotency/restart e2e, routes, request-auth, default-bot-cwd, presets e2e, team-share e2e, scripts: 39 files / 357 tests passed, 21 files skipped by their own platform guards.
  • pnpm typecheck ✓, pnpm lint ✓, pre-push hook (lint, typecheck, locale check) ✓, node --test scripts/lazurio-release-contract.node-test.mjs 5/5 ✓, overlay guard simulated locally against v0.1.91 (no merge commits, no unexpected upstream file) ✓.
  • npx vitest run server/index.test.ts: 260 passed.
  • Lazurio Fork CI on this head (34ad1eb): Overlay, typecheck and lint ✓, vitest shards 1-4 ✓, Archive linux-x64 ✓.
  • Base note: this branch stands on Lazurio distribution: release pipeline on v0.1.91 and OMB_DEFAULT_BOT_CWD (DEV-6632 M1) #1 at 34d7ac2; Lazurio distribution: release pipeline on v0.1.91 and OMB_DEFAULT_BOT_CWD (DEV-6632 M1) #1 has since moved to fefd543 (archive cleanup + LAZURIO.md paragraph). git merge-tree shows no conflict and GitHub reports the PR mergeable; it was pushed before the base moved and is not force-pushed. Rebase when retargeting to main.
  • Not verified here: a real gh against GitHub, the full upstream suite (runs in Lazurio Fork CI), and a real VM.

Risks

  • GitHub search is eventually consistent and relies on updated_at moving on push/comment/assignment; a missed bump delays work until the PR changes again.
  • Organization scope reviews every open ready PR on first enable (by design: "any open non-draft PR"); the operator should expect a backlog wave, throttled by the webhook queue (6) and rate limit.
  • The target resolves by name each poll; renaming the leader without updating OMB_GITHUB_INTAKE_BOT stops the intake (no_target, visible in status).
  • Unattended runs on Ask wait for approvals until the headless approval decision is made.

Open questions

  1. Headless approval level for the Steward team (security decision, out of scope).
  2. Should the Steward team file ship inside the release archive (so the Platform preset can import it without repo access), or should the preset fetch it from the tagged source?
  3. Publication instruction via review comments or PR review bodies is not recognised, only PR conversation comments. OK?

Plan: DEV-6632 (M2, code part). Decision: 0169.

🤖 Generated with Claude Code

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty — Changes requested on exact head 61d1fee59c1d4be6729bb3b051ba6acba165cb87 (DEV-6632 M2 / decision 0169, stacked on #1).

  1. Blocking — a publish marker inside a Markdown code fence is accepted as an instruction. hasPublishMarker() in server/github-intake.ts:109-119 toggles the fence state on any line starting with either ``` or ~~~, without matching the opening delimiter's type or length. I exercised the actual exported function on this head: \```markdown\n```\n/lazurio publish\n````` and `~~~markdown\n```\n/lazurio publish\n~~~` both return `true`. The marker remains inside a valid Markdown fenced code block, but `publications()` then treats it as a publication instruction from a writer and queues the publish bot. This violates the explicit-instruction gate and can turn a quoted example into a merge request. Match closing fences to the opener (character and minimum length), with a regression test through the publication trigger.

  2. Blocking — the after-assignment gate accepts a comment from before assignment in the same timestamp second. server/github-intake.ts:651-665 compares GitHub's second-resolution created_at values with >=. If a writer comments /lazurio publish and the account is assigned later in the same second, both timestamps compare equal and the earlier comment passes. Fail closed on equal timestamps (or establish order with reliable event identity), and test both orderings inside one second.

Local verification in an isolated detached clone: focused intake/team/e2e tests 34/34 passed; pnpm typecheck, pnpm lint, and git diff --check passed. This is a code-path finding, not a real-GitHub or VM smoke. No merge/deploy or repository changes were made.

@immakermatty
immakermatty force-pushed the lazurio/m2-github-intake branch from 61d1fee to f91ccac Compare September 29, 2026 23:22
immakermatty and others added 3 commits September 30, 2026 01:38
A Lazurio Environment's bot team should pick up GitHub work by itself
without a model polling: plain code asks GitHub, a bot is woken only for
real work (Lazurio decision 0169, plan DEV-6632 M2). Any operator can use
it, for example for pull requests sent to them.

With OMB_GITHUB_INTAKE=1 and OMB_GITHUB_INTAKE_BOT (a bot id or exact
name) the server polls GitHub through the signed-in gh CLI of its own user
(no token of its own) every 90 s by default:

- review: an open, ready pull request where the account is a requested
  reviewer, which it reviewed before, or (OMB_GITHUB_INTAKE_SCOPE=
  organization) in a repository it can push to minus
  OMB_GITHUB_INTAKE_EXCLUDE. Once per (repo, PR, head SHA); a new head or
  draft -> ready fires again; skipped when the account already approved
  or requested changes on that head.
- publication: a pull request assigned to the account plus a comment with
  the line "/lazurio publish" by someone with write, maintain or admin,
  written after the assignment. Once per instruction comment; assignment
  alone never publishes.
- the account's own pull requests are never reviewed by it; they are
  recorded as needs_human_approval without a bot turn.
- OMB_GITHUB_INTAKE_OWNERS limits every trigger to given owners.

Events go through a managed "GitHub intake" webhook and the new
WebhookManager.deliver(): the same queue, receipts, limits, attempt log
and UNTRUSTED event wrapping as an HTTP delivery. Dedupe state lives in
<data>/github-intake.json; webhook receipts still stop a second run if it
is lost. Details of a pull request are fetched only when it changed, gh
calls are bounded to four at once, rate limits back off up to 30 min, a
paused webhook or full queue keeps work pending (one refused attempt per
poll). GET /api/github-intake shows the state, POST
/api/github-intake/poll polls now (admin scope by default).

Unset keeps upstream behaviour. Upstream files changed: server/webhooks.ts
(deliver, allowlisted in lazurio-fork-ci.yml), server/index.ts and
scripts/control-omb.ts (wiring, already allowlisted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The team Lazurio Organizations run behind the GitHub intake (decision
0169, plan DEV-6632 M2), shipped as a portable openmaus.package v2 file:
lazurio/teams/steward.openmaus.json.

- Leader "Henry", the team's Chief of Staff: hands each intake event to
  one worker with delegate_bot, up to three pull requests in parallel,
  checks the result and reports failures to the operator.
- Three workers ("Steward 1-3"): one worktree per pull request, exact-head
  review submitted with gh on the given commit and verified afterwards,
  publication only for an assigned pull request with a "/lazurio publish"
  instruction and only with approvals by others and green checks on the
  current head; never approve the account's own pull request.
- Team brief: start in the Lazurio Folder and follow its AGENTS.md
  cascade; GitHub only through the signed-in account; untrusted input
  stays data.

Nothing names the persona, so Pablo imports the same file and renames
the leader. No secrets, no Organization names. A test parses the file
with the package validator, checks the secret and import scans and that
the brief carries the intake's publish marker; the intake e2e now imports
the team and hands work to its leader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment

Review of 61d1fee (Pablo):
- hasPublishMarker toggled a fence on any line starting with ``` or ~~~.
  A marker quoted inside a longer or tilde fence counted as a publication
  instruction. Fences now follow CommonMark: the opener may be indented
  at most three spaces, and the closer must use the same character, be at
  least as long and carry nothing else. An unclosed fence runs to the end
  of the comment, and lines indented four spaces or a tab are code.
- The after-assignment gate compared GitHub's one-second timestamps with
  >=, so a comment written just before the assignment in the same second
  passed. The comparison is now strict and fails closed.

Regression tests cover both, including the whole publication trigger and
both orderings within one second. They fail on the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty force-pushed the lazurio/m2-github-intake branch from f91ccac to b202ca8 Compare September 29, 2026 23:39
@immakermatty
immakermatty changed the base branch from lazurio/m1-release-pipeline to main September 29, 2026 23:56

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty — Changes requested on exact head b202ca81d1ea436205ab8adfa3d73df698911b46 (base 8ccfbb8a). The prior fence mismatch and same-second assignment findings are fixed, but the publish-marker gate still accepts a marker rendered as indented code.

Blocking — server/github-intake.ts:116-130: shallow rejects a tab only when it is the first character. A tab after 1–3 spaces also reaches the fourth CommonMark indentation column, but raw.startsWith("\t") is false and raw.trim() becomes the publish marker. Exact reproduction: hasPublishMarker("Example (not an instruction):\n\n \t/lazurio publish") returns true; mdast-util-from-markdown parses the marker line as a code node. In publications() the result becomes an instruction and, if the writer/assignment/time gates pass, wakes the publish bot for a quoted example. Reject indented code after tab-stop expansion and add a trigger-level regression for spaces + tab before the marker.

Isolated detached clone: focused intake/team/e2e/agents-catalog tests 77/77 pass; git diff --check passes. Fork CI is green on this head; these tests omit the spaces-plus-tab case. No merge, deploy or repository source changes.

…d code

Re-review of b202ca8: a tab after one to three spaces reaches the fourth
CommonMark column, so the line is indented code. The old check caught a tab
only at the start of the line, and ' \t/lazurio publish' counted as an
instruction. Leading whitespace is now measured in columns with tab stops of
4. Regression tests cover the exact repro, two- and three-space variants and
the whole publication trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty — APPROVED on exact head 43b6a8fa5a3f72c559e9869f57ce1c92eb2e2774 (base 8ccfbb8a). The prior spaces-plus-tab blocker is fixed: leadingColumns expands tabs to CommonMark 4-column stops, and the resulting shallowness gate covers both fence lines and the marker itself. The exact prior reproduction and the two-/three-space variants now return false; the publication-trigger regression confirms the example does not enqueue a publish event. Fence character/length and strict post-assignment checks remain unchanged.

Verified in a clean detached temporary clone at this SHA: intake, team, e2e, and agents-catalog-wire suites 78/78 passed serially; git diff --check passed. Live fork CI: six successes, one skipped check; no unresolved review threads or pending review requests at submission. Review only; no merge, deploy, or source edits.

@immakermatty
immakermatty merged commit 282dd08 into main Sep 30, 2026
7 checks passed
@immakermatty
immakermatty deleted the lazurio/m2-github-intake branch September 30, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants