Skip to content

Widen babysitter intake to routed pull requests - #223

Open
khaliqgant wants to merge 4 commits into
mainfrom
codex/222-routed-pr-babysitter
Open

Widen babysitter intake to routed pull requests#223
khaliqgant wants to merge 4 commits into
mainfrom
codex/222-routed-pr-babysitter

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 7, 2026

Copy link
Copy Markdown
Member

Closes #222.

What changed

  • Adds an explicit babysitter.mode: routed-open-prs intake that scans only repositories named by repos.names.
  • Reuses the standalone definition-of-done behavior for PRs without a Factory dispatch lifecycle: the PR title and body become the spec, with no issue state transition or merge.
  • Adds durable, atomic work-unit claims keyed by normalized workspace + owner/repo#PR; issue-created and routed intake share the same claim before spawn.
  • Adds pre-spawn opt-outs through the default factory:skip-babysitter label and configured excludePullRequests identities. Automatic prompts re-check labels before every provider write.
  • Bounds routed admission by batchSize, admits at most one new PR per sweep, renews active leases, and logs exact discovery/admission/defer/failure counts.
  • Keeps human notifications, mentions, status comments, and escalation off by default with notifyHumans: false.
  • Documents read-only verification and updates the feature catalog.

The volume controls are grounded in the 2026-08-07 GitHub API measurement recorded in #222: 302 open PRs, 248 ready for review, across 41 repositories.

Safety and sequencing

This PR is safe to land before #221 only because factory-created remains the default mode. Do not enable routed-open-prs until #221's GitHub App identity path is deployed and validated; otherwise broad automated writes could still be attributed to the local user.

No live sweep was run and no other PR was written to. The widened discovery path is independent of issue triage/dispatch and therefore does not assume the currently degraded dispatcher or Relayfile WebSocket path works.

Validation

All checks are reported by exit code:

  • npm run build — exit 0
  • npm run featuremap:check — exit 0
  • npm test -- --run — exit 0; 80 files, 1,443 tests passed
  • git diff --check — exit 0

The tests cover routed-only scope, incomplete metadata fail-closed behavior, label/config opt-out before spawn, notification suppression, atomic contention across memory and file stores, capacity, revision-based re-admission, and issue/routed integration.


Summary by cubic

Adds a new routed-open-prs babysitter mode to adopt existing open PRs from routed repos, with durable PR-scoped claims shared with the issue path and generation-fenced leases to prevent stale transitions. Addresses Linear #222 while keeping the default issue-driven mode unchanged and notifications off unless explicitly enabled.

  • New Features

    • babysitter.mode: routed-open-prs scans only repos.names; admits open, non-draft, same-repo PRs, and uses the PR title/body as the spec when no issue lifecycle exists.
    • Durable PR work-unit claims keyed by owner/repo#number, with restart-safe leases, claimId-fenced generations, guarded adoption (no stealing live leases), and cross-path conflict suppression.
    • Bounded intake: respects batchSize, renews leases, starts at most one new routed PR per sweep, and logs detailed discovery/admission/deferral counters.
    • Opt-outs via babysitter.excludeLabels (default factory:skip-babysitter) and babysitter.excludePullRequests; routed agents re-check labels before their first and every later provider write; babysitter.notifyHumans defaults to false; explicit CLI babysits keep notifications on; task template includes opt-out and notification flags.
    • Orchestrator/state store add claim/adopt/run/complete/release/list APIs with completed-claim retention and restart restoration; strict config validation requires repos.names to resolve at least one owner/repo.
  • Migration

Written for commit fd3769d. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds routed open-PR discovery, durable per-PR ownership, capacity-bounded babysitter admission, restart recovery, and pre-write opt-out checks. It also extends configuration, prompts, tests, documentation, verification procedures, and feature catalog counts.

Changes

Routed PR babysitter

Layer / File(s) Summary
Discovery contract and candidate filtering
src/config/schema.ts, src/github/routed-pr-babysitter.ts, src/github/index.ts, src/github/routed-pr-babysitter.test.ts
Adds routed intake configuration and utilities that scope, parse, deduplicate, filter, and report eligible pull requests.
Durable claim state
src/ports/state.ts, src/state/file-state-store.ts, src/state/in-memory-state-store.ts, src/state/routed-pr-babysitter-claim.test.ts
Adds lease-based claim lifecycle operations to both state stores, including capacity checks, ownership validation, persistence, migration, and revision-aware re-admission.
Sweep admission and ownership coordination
src/orchestrator/factory.ts, src/orchestrator/factory.test.ts
Adds routed sweeps across startup, polling, run-once, completion, and PR activity paths. It restores claims, limits admission, spawns standalone babysitters, handles exits, and coordinates issue-created ownership.
Provider-write safeguards and catalog updates
src/dispatch/templates.ts, src/dispatch/templates.test.ts, src/cli/fleet.ts, README.md, .agentworkforce/features/verify/procedures.md, .agentworkforce/features/manifest.yaml, .agentworkforce/agents/factory-feature-guardian/agent.test.ts
Adds label rechecks, configurable notifications, CLI behavior, documentation, verification guidance, and updated feature catalog totals.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FactoryLoop
  participant Discovery
  participant StateStore
  participant Babysitter
  FactoryLoop->>Discovery: Scan configured repositories
  Discovery-->>FactoryLoop: Return eligible PR candidates
  FactoryLoop->>StateStore: Claim candidate with lease
  StateStore-->>FactoryLoop: Confirm ownership or defer
  FactoryLoop->>Babysitter: Spawn standalone PR task
  Babysitter->>StateStore: Complete or release claim on exit
Loading

Possibly related PRs

Suggested reviewers: kjgbot

Poem

A rabbit finds each routed PR,
Claims its work both near and far.
Leases guard the task at night,
Labels stop an unsafe write.
Quiet hops unless humans say,
Four new features join the day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy the coding objectives in [#222], including scoped discovery, claims, opt-outs, bounded admission, reporting, and notification controls.
Out of Scope Changes check ✅ Passed The implementation, tests, documentation, and feature metadata support the routed babysitter objectives without unrelated code changes.
Title check ✅ Passed The title clearly and concisely describes the main change: adding routed pull requests to babysitter intake.
Description check ✅ Passed The description directly explains the routed PR intake, claims, safeguards, limits, validation, and deployment conditions.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/222-routed-pr-babysitter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@khaliqgant
khaliqgant marked this pull request as ready for review August 8, 2026 11:12
@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 714a7ba5bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/github/routed-pr-babysitter.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/github/routed-pr-babysitter.test.ts (1)

33-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten the fixture and cover a rejecting listTree.

Two small points:

  • Line 68 spreads files and then overwrites the same key, so the spread has no effect. Pass the single record directly to state the intent.
  • The fake listTree never rejects, so the discovery failure path stays untested. Add a case where one alias root rejects. That case also documents the expected behavior for the per-root failure handling raised in src/github/routed-pr-babysitter.ts.
🧪 Proposed change
     const byIdentity = await discoverRoutedPullRequests(
-      mount({ ...files, '/github/repos/AgentWorkforce__pear/pulls/by-id/7.json': pull() }),
+      mount({ '/github/repos/AgentWorkforce__pear/pulls/by-id/7.json': pull() }),
       config({ excludePullRequests: ['AgentWorkforce/pear#7'] }),
     )

Also applies to: 67-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github/routed-pr-babysitter.test.ts` around lines 33 - 36, Update the
fixture setup around mount and the affected test to pass the single file record
directly instead of spreading and overwriting files. Extend the MountClient fake
or add a focused test using listTree to reject for one alias root, and assert
the discovery flow’s expected per-root failure handling in the routed PR
babysitter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/github/routed-pr-babysitter.ts`:
- Around line 77-89: Update the root-scanning loop in the repository path around
roots and MountClient.listTree so each root is handled in its own try/catch.
Record the failure for a rejected root, then continue scanning the remaining
alias root instead of aborting the entire repository scan; preserve adding
successful results to paths and the existing failure reporting format.

In `@src/orchestrator/factory.ts`:
- Around line 1697-1705: Update `#sweepRoutedPrBabysitters` to handle failures
from `#runRoutedPrBabysitterSweep` at its own boundary by attaching a catch
handler that logs the error and resolves without rethrowing, while retaining the
existing finally cleanup and in-flight promise deduplication. Ensure callers
such as runOnce and the fire-and-forget invocation in `#handlePrChange` cannot
have routed sweep errors abort the primary loop or become unhandled rejections.
- Around line 5876-5889: Update the routed babysitter exit branch around
`#routedPrBabysitterAgents` and completeRoutedPrBabysitter so it only marks the
durable claim complete when isCompletionReason(reason) is true. For crash,
reconciled-missing, registration-lag, delivery failure, and other non-completion
reasons, preserve the interrupted session instead of calling
completeRoutedPrBabysitter, while retaining cleanup and metrics behavior
consistent with the issue-created counterpart.
- Around line 1679-1695: Wrap the body of `#restoreRoutedPrBabysitterClaims` in a
try/catch so failures from fleet.roster or listRoutedPrBabysitterClaims do not
propagate into `#start` and abort daemon startup. Log a warning with the existing
logger, matching the fail-closed error-handling pattern used by
`#adoptInFlightAgents`, while preserving the current claim-restoration behavior on
success.
- Around line 1785-1806: Track the failed spawn in the per-sweep stats used by
the trailing capacityDeferred calculation: add or increment a dedicated failure
count in the catch around `#spawnRoutedPrBabysitter`, and subtract that count from
the remaining-candidates calculation. Preserve the existing claim release,
cumulative routedPrBabysitterSpawnFailures increment, and warning log behavior.

In `@src/state/file-state-store.ts`:
- Around line 676-695: The routed PR claim mutation must prune completed claims
older than a bounded retention age before checking the existing claim. Apply
this cleanup inside the same mutation lock in src/state/file-state-store.ts
lines 676-695 and persist the pruned document, then apply the identical rule in
src/state/in-memory-state-store.ts lines 639-675 so both stores produce matching
outcomes for aged claims.

---

Nitpick comments:
In `@src/github/routed-pr-babysitter.test.ts`:
- Around line 33-36: Update the fixture setup around mount and the affected test
to pass the single file record directly instead of spreading and overwriting
files. Extend the MountClient fake or add a focused test using listTree to
reject for one alias root, and assert the discovery flow’s expected per-root
failure handling in the routed PR babysitter.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c244e4e9-3bdb-495f-ac7f-83567af69678

📥 Commits

Reviewing files that changed from the base of the PR and between 16ad97e and 714a7ba.

📒 Files selected for processing (18)
  • .agentworkforce/agents/factory-feature-guardian/agent.test.ts
  • .agentworkforce/features/manifest.yaml
  • .agentworkforce/features/verify/procedures.md
  • README.md
  • src/cli/fleet.ts
  • src/config/schema.test.ts
  • src/config/schema.ts
  • src/dispatch/templates.test.ts
  • src/dispatch/templates.ts
  • src/github/index.ts
  • src/github/routed-pr-babysitter.test.ts
  • src/github/routed-pr-babysitter.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/state.ts
  • src/state/file-state-store.ts
  • src/state/in-memory-state-store.ts
  • src/state/routed-pr-babysitter-claim.test.ts

Comment thread src/github/routed-pr-babysitter.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/state/file-state-store.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 18 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/ports/state.ts
Comment thread src/state/in-memory-state-store.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/cli/fleet.ts
Comment thread src/config/schema.ts
Comment thread src/orchestrator/factory.ts
Comment thread src/github/routed-pr-babysitter.ts Outdated
@kjgbot
kjgbot force-pushed the codex/222-routed-pr-babysitter branch from 714a7ba to 8055748 Compare August 8, 2026 20:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/state/in-memory-state-store.ts (1)

640-688: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the shared claim algorithm.

claimRoutedPrBabysitter here and in src/state/file-state-store.ts lines 655-708 implement the same seven-step algorithm against two different containers (Map and plain object). The two copies must stay identical, because src/state/routed-pr-babysitter-claim.test.ts asserts equal outcomes for both stores. A shared pure helper that takes get/set/delete/entries accessors would remove the drift risk.

This matches the existing dispatch-lifecycle duplication in the same files, so it is optional for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/in-memory-state-store.ts` around lines 640 - 688, Optionally
extract the shared claim algorithm used by
InMemoryStateStore.claimRoutedPrBabysitter and
FileStateStore.claimRoutedPrBabysitter into one pure helper parameterized by
get, set, delete, and entries accessors. Keep the seven-step behavior and
outcomes identical for Map and plain-object storage, and update both store
methods to delegate to the helper.
src/state/routed-pr-babysitter-claim.test.ts (1)

197-203: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the temporary directories and add two negative cases.

Each file state test creates a directory under tmpdir() and never removes it. Add an afterEach or afterAll cleanup that removes the created roots.

The suite also does not cover two owner guards that the implementations enforce: releaseRoutedPrBabysitterClaim returning true for the correct owner, and markRoutedPrBabysitterRunning returning false for a wrong owner. Add those cases so both stores stay aligned on ownership checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/routed-pr-babysitter-claim.test.ts` around lines 197 - 203, Update
the routed PR babysitter claim tests around verifyClaims to track each
file-state temporary root and remove it in afterEach or afterAll. Add coverage
for releaseRoutedPrBabysitterClaim returning true for the owning caller and
markRoutedPrBabysitterRunning returning false for a non-owner, ensuring both
memory and file stores exercise these ownership guards.
src/orchestrator/factory.test.ts (1)

16325-16372: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a bound on routed retries.

The test emits one crash exit and asserts two spawns. It does not assert an upper bound on retries. If the sweep respawned on every crash without a limit, this test would still pass. A crash-looping PR would then consume a work-unit slot forever.

Add a case that emits repeated crash exits and asserts the spawn count stops increasing, or confirm that a retry limit exists in src/orchestrator/factory.ts.

#!/bin/bash
# Description: Check for a retry bound on routed PR babysitter respawns.
rg -n -C 6 'routedPrBabysitter' src/orchestrator/factory.ts | rg -n -C 6 'attempt|retry|max|limit'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/orchestrator/factory.test.ts` around lines 16325 - 16372, Bound routed PR
babysitter respawns in the factory flow, using the retry/attempt tracking around
routedPrBabysitter in factory.ts so repeated crash exits stop creating new
spawns after the configured maximum. Update the test case to emit repeated crash
exits and assert the spawn count no longer increases once the retry limit is
reached, while preserving the existing successful-exit completion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/state/file-state-store.ts`:
- Around line 729-747: Update adoptRoutedPrBabysitterClaim in
src/state/file-state-store.ts (lines 729-747) and
src/state/in-memory-state-store.ts (lines 705-719) to reject running claims with
a live lease unless the existing claim owner matches the adopting owner; permit
adoption only when claim.leaseUntilMs <= nowMs or ownership matches, preserving
the current mutation and persistence behavior.

---

Nitpick comments:
In `@src/orchestrator/factory.test.ts`:
- Around line 16325-16372: Bound routed PR babysitter respawns in the factory
flow, using the retry/attempt tracking around routedPrBabysitter in factory.ts
so repeated crash exits stop creating new spawns after the configured maximum.
Update the test case to emit repeated crash exits and assert the spawn count no
longer increases once the retry limit is reached, while preserving the existing
successful-exit completion behavior.

In `@src/state/in-memory-state-store.ts`:
- Around line 640-688: Optionally extract the shared claim algorithm used by
InMemoryStateStore.claimRoutedPrBabysitter and
FileStateStore.claimRoutedPrBabysitter into one pure helper parameterized by
get, set, delete, and entries accessors. Keep the seven-step behavior and
outcomes identical for Map and plain-object storage, and update both store
methods to delegate to the helper.

In `@src/state/routed-pr-babysitter-claim.test.ts`:
- Around line 197-203: Update the routed PR babysitter claim tests around
verifyClaims to track each file-state temporary root and remove it in afterEach
or afterAll. Add coverage for releaseRoutedPrBabysitterClaim returning true for
the owning caller and markRoutedPrBabysitterRunning returning false for a
non-owner, ensuring both memory and file stores exercise these ownership guards.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 120d73ac-384b-487b-96a3-864ac4a5f9be

📥 Commits

Reviewing files that changed from the base of the PR and between 714a7ba and 8055748.

📒 Files selected for processing (11)
  • src/cli/fleet.ts
  • src/config/schema.test.ts
  • src/config/schema.ts
  • src/github/routed-pr-babysitter.test.ts
  • src/github/routed-pr-babysitter.ts
  • src/orchestrator/factory.test.ts
  • src/orchestrator/factory.ts
  • src/ports/state.ts
  • src/state/file-state-store.ts
  • src/state/in-memory-state-store.ts
  • src/state/routed-pr-babysitter-claim.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/cli/fleet.ts
  • src/config/schema.ts
  • src/github/routed-pr-babysitter.ts
  • src/orchestrator/factory.ts

Comment thread src/state/file-state-store.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 18 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/github/routed-pr-babysitter.ts Outdated
Comment thread src/ports/state.ts Outdated
Comment thread src/ports/state.ts Outdated
Comment thread src/orchestrator/factory.ts
Comment thread src/orchestrator/factory.ts Outdated
Comment thread src/config/schema.ts Outdated
Comment thread src/config/schema.ts
Comment thread src/state/file-state-store.ts
Comment thread src/state/in-memory-state-store.ts Outdated
Comment thread src/orchestrator/factory.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/state/in-memory-state-store.ts">

<violation number="1" location="src/state/in-memory-state-store.ts:705">
P1: An agent completing after its lease expires can mark stale work complete and suppress re-admission for 30 days; require a live lease when completing the claim.</violation>
</file>

<file name="src/orchestrator/factory.ts">

<violation number="1" location="src/orchestrator/factory.ts:6035">
P2: An abnormal issue-created babysitter exit leaves its durable claim running after its token is discarded, so a recovered worker's later successful exit cannot complete it. Release `issueClaim` on non-completion before deleting the local entry, matching routed-worker handling.</violation>

<violation number="2" location="src/orchestrator/factory.ts:10738">
P2: On the new `if (!markedRunning)` claim-lost guard in #ensureBabysitter, the code releases the agent and returns without clearing the `#babysitterSpawned`/`#babysitterPr` bookkeeping that was set up just before. Because a subsequent #ensureBabysitter for the same issue+PR short-circuits on `#babysitterSpawned.has(babysitterKey)` (awaiting the in-flight promise and reading the stale `#babysitterPr` entry), the babysitter for that PR is never re-spawned within this process — it stays wedged until a restart. The new fail-closed guard is meant to recover from a lost claim, so it should also roll back the pre-spawn markers (and release the claim if it still exists) so a later event can retry.</violation>
</file>

<file name="src/config/schema.ts">

<violation number="1" location="src/config/schema.ts:134">
P2: The `babysitter.excludePullRequests` regex was changed from `[A-Za-z0-9_.-]{1,100}` to `[A-Za-z0-9_.-]{0,99}` for the repository segment. Lowering the minimum to 0 means the validator now accepts an empty repository segment, e.g. `owner/#1`, which is not a real GitHub identity and can never match a scanned PR (so an operator typo silently does nothing), while simultaneously lowering the max to 99 so a legitimate 100-character repository name (GitHub's documented upper bound) is now rejected as 'expected owner/repo#number'. The added test only covers the 1-character repo `owner/r#1`, which already validated under the previous `{1,100}` range, so this regex edit does not appear necessary to achieve the stated goal. Consider restoring a 1..N range with a required first character (e.g. `[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})` to keep 1–100 while mirroring the owner segment's shape).</violation>

<violation number="2" location="src/config/schema.ts:340">
P3: The new `requireRoutedBabysitterRepos` resolution check only applies the org prefix in its final fallback (`org ? `${org}/${name}` : name`), but the runtime path in `routedPrRepos` (src/github/routed-pr-babysitter.ts) applies the org prefix whenever the resolved value lacks a slash, including when it came from an explicit `byLabel` entry. So a config where `byLabel` maps a name to a bare repo name while `repos.org` is set would be rejected by this schema check ('must resolve at least one owner/repository route') even though `routedPrRepos` would route it as `${org}/${name}` at runtime. Applying the same org-prefix logic to the byLabel/overrides results would keep the schema guard and the runtime scanner consistent.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const claim = this.#workspace(workspaceId).routedPrBabysitterClaims.get(identity)
if (
!claim ||
claim.owner !== owner ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: An agent completing after its lease expires can mark stale work complete and suppress re-admission for 30 days; require a live lease when completing the claim.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/state/in-memory-state-store.ts, line 705:

<comment>An agent completing after its lease expires can mark stale work complete and suppress re-admission for 30 days; require a live lease when completing the claim.</comment>

<file context>
@@ -691,11 +695,18 @@ export class InMemoryStateStore implements StateStore {
-    if (!claim || claim.owner !== owner || claim.status === 'complete') return false
+    if (
+      !claim ||
+      claim.owner !== owner ||
+      claim.claimId !== claimId ||
+      claim.status === 'complete' ||
</file context>

Comment on lines +6035 to +6046
const issueClaim = this.#issueBabysitterClaims.get(name)
if (isCompletionReason(reason) && exiting?.spec.role === 'babysitter' && issueClaim) {
await this.#state.completeRoutedPrBabysitter(
this.#workspaceId,
issueClaim.identity,
this.#dispatchLifecycleOwner,
issueClaim.claimId,
name,
this.#clock.now(),
)
}
this.#issueBabysitterClaims.delete(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: An abnormal issue-created babysitter exit leaves its durable claim running after its token is discarded, so a recovered worker's later successful exit cannot complete it. Release issueClaim on non-completion before deleting the local entry, matching routed-worker handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/orchestrator/factory.ts, line 6035:

<comment>An abnormal issue-created babysitter exit leaves its durable claim running after its token is discarded, so a recovered worker's later successful exit cannot complete it. Release `issueClaim` on non-completion before deleting the local entry, matching routed-worker handling.</comment>

<file context>
@@ -6008,14 +6032,18 @@ export class FactoryLoop implements Factory {
 
     const exiting = record.agents.get(name)
-    if (isCompletionReason(reason) && exiting?.spec.role === 'babysitter' && exiting.spec.ownedPullRequest) {
+    const issueClaim = this.#issueBabysitterClaims.get(name)
+    if (isCompletionReason(reason) && exiting?.spec.role === 'babysitter' && issueClaim) {
       await this.#state.completeRoutedPrBabysitter(
</file context>
Suggested change
const issueClaim = this.#issueBabysitterClaims.get(name)
if (isCompletionReason(reason) && exiting?.spec.role === 'babysitter' && issueClaim) {
await this.#state.completeRoutedPrBabysitter(
this.#workspaceId,
issueClaim.identity,
this.#dispatchLifecycleOwner,
issueClaim.claimId,
name,
this.#clock.now(),
)
}
this.#issueBabysitterClaims.delete(name)
const issueClaim = this.#issueBabysitterClaims.get(name)
if (exiting?.spec.role === 'babysitter' && issueClaim) {
if (isCompletionReason(reason)) {
await this.#state.completeRoutedPrBabysitter(
this.#workspaceId,
issueClaim.identity,
this.#dispatchLifecycleOwner,
issueClaim.claimId,
name,
this.#clock.now(),
)
} else {
await this.#state.releaseRoutedPrBabysitterClaim(
this.#workspaceId,
issueClaim.identity,
this.#dispatchLifecycleOwner,
issueClaim.claimId,
)
}
}
this.#issueBabysitterClaims.delete(name)

Comment thread src/config/schema.ts
// before their first provider write.
excludeLabels: z.array(z.string().trim().min(1)).default(['factory:skip-babysitter']),
excludePullRequests: z.array(z.string().regex(
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9_.-]{0,99}#[1-9]\d*$/u,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The babysitter.excludePullRequests regex was changed from [A-Za-z0-9_.-]{1,100} to [A-Za-z0-9_.-]{0,99} for the repository segment. Lowering the minimum to 0 means the validator now accepts an empty repository segment, e.g. owner/#1, which is not a real GitHub identity and can never match a scanned PR (so an operator typo silently does nothing), while simultaneously lowering the max to 99 so a legitimate 100-character repository name (GitHub's documented upper bound) is now rejected as 'expected owner/repo#number'. The added test only covers the 1-character repo owner/r#1, which already validated under the previous {1,100} range, so this regex edit does not appear necessary to achieve the stated goal. Consider restoring a 1..N range with a required first character (e.g. [A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99}) to keep 1–100 while mirroring the owner segment's shape).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/config/schema.ts, line 134:

<comment>The `babysitter.excludePullRequests` regex was changed from `[A-Za-z0-9_.-]{1,100}` to `[A-Za-z0-9_.-]{0,99}` for the repository segment. Lowering the minimum to 0 means the validator now accepts an empty repository segment, e.g. `owner/#1`, which is not a real GitHub identity and can never match a scanned PR (so an operator typo silently does nothing), while simultaneously lowering the max to 99 so a legitimate 100-character repository name (GitHub's documented upper bound) is now rejected as 'expected owner/repo#number'. The added test only covers the 1-character repo `owner/r#1`, which already validated under the previous `{1,100}` range, so this regex edit does not appear necessary to achieve the stated goal. Consider restoring a 1..N range with a required first character (e.g. `[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})` to keep 1–100 while mirroring the owner segment's shape).</comment>

<file context>
@@ -131,7 +131,7 @@ const babysitterSchema = z.object({
   excludeLabels: z.array(z.string().trim().min(1)).default(['factory:skip-babysitter']),
   excludePullRequests: z.array(z.string().regex(
-    /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9_.-]{1,100}#[1-9]\d*$/u,
+    /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9_.-]{0,99}#[1-9]\d*$/u,
     'expected owner/repo#number',
   )).default([]),
</file context>
Suggested change
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9_.-]{0,99}#[1-9]\d*$/u,
/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})\/[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})#[1-9]\d*$/u,

tracked.result?.name ?? trackedName,
this.#clock.now(),
)
if (!markedRunning) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: On the new if (!markedRunning) claim-lost guard in #ensureBabysitter, the code releases the agent and returns without clearing the #babysitterSpawned/#babysitterPr bookkeeping that was set up just before. Because a subsequent #ensureBabysitter for the same issue+PR short-circuits on #babysitterSpawned.has(babysitterKey) (awaiting the in-flight promise and reading the stale #babysitterPr entry), the babysitter for that PR is never re-spawned within this process — it stays wedged until a restart. The new fail-closed guard is meant to recover from a lost claim, so it should also roll back the pre-spawn markers (and release the claim if it still exists) so a later event can retry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/orchestrator/factory.ts, line 10738:

<comment>On the new `if (!markedRunning)` claim-lost guard in #ensureBabysitter, the code releases the agent and returns without clearing the `#babysitterSpawned`/`#babysitterPr` bookkeeping that was set up just before. Because a subsequent #ensureBabysitter for the same issue+PR short-circuits on `#babysitterSpawned.has(babysitterKey)` (awaiting the in-flight promise and reading the stale `#babysitterPr` entry), the babysitter for that PR is never re-spawned within this process — it stays wedged until a restart. The new fail-closed guard is meant to recover from a lost claim, so it should also roll back the pre-spawn markers (and release the claim if it still exists) so a later event can retry.</comment>

<file context>
@@ -10660,13 +10727,22 @@ export class FactoryLoop implements Factory {
         tracked.result?.name ?? trackedName,
         this.#clock.now(),
       )
+      if (!markedRunning) {
+        await this.#fleet.release(tracked.result?.name ?? trackedName, 'pr-work-claim-lost')
+        return
</file context>

Comment thread src/config/schema.ts
}
if (cfg.babysitter.mode === 'routed-open-prs') {
const routedRepos = (cfg.repos.names ?? []).map((name) =>
cfg.repos.byLabel[name] ?? cfg.repos.overrides[name] ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new requireRoutedBabysitterRepos resolution check only applies the org prefix in its final fallback (org ? ${org}/${name} : name), but the runtime path in routedPrRepos (src/github/routed-pr-babysitter.ts) applies the org prefix whenever the resolved value lacks a slash, including when it came from an explicit byLabel entry. So a config where byLabel maps a name to a bare repo name while repos.org is set would be rejected by this schema check ('must resolve at least one owner/repository route') even though routedPrRepos would route it as ${org}/${name} at runtime. Applying the same org-prefix logic to the byLabel/overrides results would keep the schema guard and the runtime scanner consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/config/schema.ts, line 340:

<comment>The new `requireRoutedBabysitterRepos` resolution check only applies the org prefix in its final fallback (`org ? `${org}/${name}` : name`), but the runtime path in `routedPrRepos` (src/github/routed-pr-babysitter.ts) applies the org prefix whenever the resolved value lacks a slash, including when it came from an explicit `byLabel` entry. So a config where `byLabel` maps a name to a bare repo name while `repos.org` is set would be rejected by this schema check ('must resolve at least one owner/repository route') even though `routedPrRepos` would route it as `${org}/${name}` at runtime. Applying the same org-prefix logic to the byLabel/overrides results would keep the schema guard and the runtime scanner consistent.</comment>

<file context>
@@ -333,6 +333,20 @@ const requireRoutedBabysitterRepos = (
+  }
+  if (cfg.babysitter.mode === 'routed-open-prs') {
+    const routedRepos = (cfg.repos.names ?? []).map((name) =>
+      cfg.repos.byLabel[name] ?? cfg.repos.overrides[name] ??
+      (cfg.repos.org ? `${cfg.repos.org}/${name}` : name)
+    )
</file context>

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.

[factory] The automatic babysitter only covers Factory-created PRs, so it shepherds almost nothing

1 participant