KAHN agent fleets ↔ PETROVA
Integration: KAHN agentic fleets ↔ Petrova control plane
Section titled “Integration: KAHN agentic fleets ↔ Petrova control plane”How KAHN-style fleets call petrova verbs to act on consumer repos without violating MR-6 (subagent additions go in AGENTS.xml), MR-7 (decisions are append-only, dated), or MR-12 (CLAUDE.md is a projection).
The boundary
Section titled “The boundary”┌──────────────────────────────────────────┐│ KAHN fleet ││ (running anywhere — local, CI, agent ││ service) ││ ││ Owns: diagnose, plan, draft fixes ││ Reads: target repo (local clone or API) ││ Writes: NOTHING DIRECTLY │└──────────────────┬───────────────────────┘ │ petrova-act verb call │ (typed JSON, dry-run first) ▼┌──────────────────────────────────────────┐│ Petrova control plane ││ (this repo + skills/petrova-act) ││ ││ Validates input against spec/verbs/ ││ Checks registry + fleets_allowed ││ Emits PR via GitHub Contents API ││ Returns idempotency-keyed result │└──────────────────┬───────────────────────┘ │ PR (branch protection, │ CODEOWNERS, CI gates) ▼┌──────────────────────────────────────────┐│ Consumer repo (kahn-hq, smo1-io, …) ││ Human or auto-merge label completes the ││ merge. Fleet never bypasses this gate. │└──────────────────────────────────────────┘The fleet’s judgement lives in its own diagnose/plan layer. The fleet’s action surface is exactly the petrova verb catalogue. Anything outside that catalogue is something the fleet must surface to a human — never improvise.
Required handshake
Section titled “Required handshake”Before a fleet can write to a target repo:
- Repo registered.
petrova-hq/registry.yamllists the repo’s slug, url, role, and profile. - Fleet allowed. The registry entry’s
fleets_allowedarray contains the fleet’s identifier (e.g.kahn-diagnostics,kahn-implementer). - Auth provisioned. Either a fine-grained PAT or a GitHub App
installation, set in the fleet’s environment as
PETROVA_GITHUB_TOKENor thePETROVA_APP_*triple. - AGENTS.xml capability declared. The consumer repo’s
AGENTS.xmllists<capability id="petrova-act">(added by TASKSET 7’s update tocore/templates/AGENTS.xml.tmpl; consumer repos inherit on bootstrap or via a request_review PR).
Once all four hold, the fleet calls verbs with actor: "fleet:<id>"
and the registry’s fleets_allowed gate enforces scope.
End-to-end flow: diagnose → propose_fix → PR
Section titled “End-to-end flow: diagnose → propose_fix → PR”# 1. Fleet runs diagnose to gather context.DIAG=$(petrova diagnose kahn-hq --json | jq -r '.result.diagnosis_id')
# 2. Fleet reads diagnose output, decides what to fix.# (This is where the fleet's own judgment runs — not petrova's job.)
# 3. Fleet composes a propose_fix payload, citing MR grounding.cat > /tmp/fix.json <<JSON{ "diagnosis_id": "$DIAG", "title": "fix(scope): live-dot for sub-second runs (M7.2.1)", "rationale": "Diagnose surfaced live-dot regression; M7.2.1 acceptance criterion not met.", "proposed_changes": [ {"path": "frontend/src/components/live-dot.ts", "operation": "modify", "contents": "...", "edit_rationale": "Lower threshold from 1000ms to 250ms."} ], "test_plan": ["vitest live-dot.spec.ts", "manual: scripts/seed-subsec-run.sh"], "mr_grounding": [ {"kind": "meta_rule", "ref": "MR-2", "claim": "M6.5 carry-over rule"}, {"kind": "decision_doc", "ref": "docs/decisions/2026-04-28-phase-6-friction-round.md", "claim": "Q1 surfaced live-dot subsec gap."} ]}JSON
# 4. Dry-run. Fleet inspects the diff.petrova propose_fix kahn-hq \ --input /tmp/fix.json \ --actor fleet:kahn-implementer \ --triggered-by-kind audit_fail \ --triggered-by-ref ci-run-12345 \ --json
# 5. If dry-run is sane and the fleet's policy allows, apply.petrova propose_fix kahn-hq \ --input /tmp/fix.json \ --actor fleet:kahn-implementer \ --triggered-by-kind audit_fail \ --triggered-by-ref ci-run-12345 \ --apply# → status: applied, PR URL printed.
# 6. PR is now open. Branch protection + CI gates own merge.# Fleet's job is done.A worked transcript with realistic output is at
docs/integrations/examples/diagnose-then-fix.md.
Failure modes and recovery
Section titled “Failure modes and recovery”Every verb invocation produces one of: dry_run, applied,
skipped_idempotent, failed. The fleet’s response per case:
| Status | Cause | Fleet response |
|---|---|---|
dry_run | Default invocation. | Inspect diff, decide whether to --apply. |
applied | New PR opened. | Record PR ref, stop. Branch protection takes over. |
skipped_idempotent | A prior run with same key already opened a PR. | Use returned PR ref; do not retry with same input. To force a new PR, change inputs (which changes the key). |
failed | One of the codes below. | Per-code recovery in the table below. |
Per-code recovery
Section titled “Per-code recovery”| Error code | What it means | Fleet action |
|---|---|---|
REPO_NOT_IN_REGISTRY | Target repo absent from registry.yaml. | Surface to human; needs onboarding PR. Do not retry. |
FLEETS_ALLOWED | Fleet ID not in this repo’s fleets_allowed. | Surface to human; registry update needed. Do not retry. |
AUTH_MISSING | No PETROVA_GITHUB_TOKEN / App env. | Fail loudly — fleet should never reach --apply without auth. Do not retry until env fixed. |
FIELD_PATTERN / FIELD_REQUIRED / FIELD_ENUM | Schema validation. | The fleet’s payload is malformed. Fix the payload. Do retry after fix. |
NO_PRIVILEGED_PATHS | Trying to edit .github/workflows/, *.env, secrets/, deploy/credentials/. | Do not retry. Surface to human; privileged paths require human edit. |
DEFERRED_HAS_TARGET | close_phase deferred item without deferred_to_milestone. | Re-emit with the target milestone in a later phase (MR-2). |
HUMAN_COUNTERSIGN_PRESENT | close_phase without sign_off.human. | Surface to human for countersign. Do not auto-fill. |
DIAGNOSIS_EXISTS / DIAGNOSIS_REPO_MATCH | propose_fix references stale or wrong-repo diagnosis. | Re-run diagnose for the correct repo, retry. |
PROFILE_PERMITS_AUTOMERGE | request_merge_when_green on strict-profile repo. | Switch to request_review and request human approval. Do not retry same verb. |
BASE_BRANCH_MISSING | Repo’s default_branch doesn’t exist. | Registry data drift. Surface to human. |
FILE_ALREADY_EXISTS | create operation on an existing path. | Either change to modify or change the path; re-emit. |
Live-state failures (caught by emitter, not schema)
Section titled “Live-state failures (caught by emitter, not schema)”These happen during --apply after schema validation passes:
- Branch already exists with different content — happens on partial
retries. The emitter no-ops the branch creation but reuses the existing
branch. If the existing branch has commits the verb didn’t make, the
PR will not match the diff preview. Fleet should
git ls-remoteto detect this before retry. - GitHub rate limit — surfaces as a 403 from Octokit. Fleet should back off (60s minimum) and retry. Idempotency means retry is safe.
- CI red on the resulting PR — not a verb failure (PR opened
successfully). Fleet may invoke
propose_fixagain with a corrected patch, OR surface the failure to a human. - Concurrent edit on default branch invalidates SHA — retry the verb; fresh SHA fetched on each apply.
Identity and audit
Section titled “Identity and audit”Every PR body the verbs emit carries:
- The idempotency key — re-runs are detected and become no-ops.
- The actor —
fleet:<id>orhuman:<email>. PR review can filter by actor in GitHub’s UI. - The trigger — what surfaced the action (
audit_fail,phase_close,friction_item, …). - The schema fingerprint — first 12 chars of the verb schema’s SHA256. If the schema changes, old PRs’ fingerprints reveal which contract version produced them.
- The MR citations — every claim about why the change is correct grounds in a specific meta-rule.
This is the audit trail that lets a future operator trace any change back to: who (which fleet), why (which trigger), under what rules (which MRs), and against what contract (which schema fingerprint).
What this integration explicitly forbids
Section titled “What this integration explicitly forbids”- Fleet writing files in a target repo’s working tree directly.
- Fleet pushing branches.
- Fleet merging PRs.
- Fleet adding/modifying secrets, workflows, deploy credentials.
- Fleet creating registry entries (onboarding is a human PR per TASKSET 8).
- Fleet inventing new verbs. The catalogue is the surface; gaps go to petrova-hq as a verb-extension proposal.
- Fleet bypassing dry-run when
--applywould emit non-trivial change. Trivial-change auto-apply is allowed only when the fleet has explicit standing instruction for that exact verb on that exact repo.
Relationship to existing KAHN concepts
Section titled “Relationship to existing KAHN concepts”KAHN’s audit_checkpoint partitions (audit-rls, audit-coverage,
audit-contracts, audit-deps, audit-citations) map onto the verb
flow as follows:
- An audit failure is a
triggered_by: audit_failevent. - The named audit agent (e.g.
audit-rls) sets theactorfield tofleet:<audit-name>. - The audit’s evidence path becomes the
triggered_by.ref. - The proposed remediation is composed via
propose_fix, with the audit’s findings cited inmr_grounding.
This makes KAHN audits first-class events in the petrova ledger without KAHN reaching into consumer repos directly.