Skip to content

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).

┌──────────────────────────────────────────┐
│ 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.

Before a fleet can write to a target repo:

  1. Repo registered. petrova-hq/registry.yaml lists the repo’s slug, url, role, and profile.
  2. Fleet allowed. The registry entry’s fleets_allowed array contains the fleet’s identifier (e.g. kahn-diagnostics, kahn-implementer).
  3. Auth provisioned. Either a fine-grained PAT or a GitHub App installation, set in the fleet’s environment as PETROVA_GITHUB_TOKEN or the PETROVA_APP_* triple.
  4. AGENTS.xml capability declared. The consumer repo’s AGENTS.xml lists <capability id="petrova-act"> (added by TASKSET 7’s update to core/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”
Terminal window
# 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.

Every verb invocation produces one of: dry_run, applied, skipped_idempotent, failed. The fleet’s response per case:

StatusCauseFleet response
dry_runDefault invocation.Inspect diff, decide whether to --apply.
appliedNew PR opened.Record PR ref, stop. Branch protection takes over.
skipped_idempotentA 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).
failedOne of the codes below.Per-code recovery in the table below.
Error codeWhat it meansFleet action
REPO_NOT_IN_REGISTRYTarget repo absent from registry.yaml.Surface to human; needs onboarding PR. Do not retry.
FLEETS_ALLOWEDFleet ID not in this repo’s fleets_allowed.Surface to human; registry update needed. Do not retry.
AUTH_MISSINGNo 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_ENUMSchema validation.The fleet’s payload is malformed. Fix the payload. Do retry after fix.
NO_PRIVILEGED_PATHSTrying to edit .github/workflows/, *.env, secrets/, deploy/credentials/.Do not retry. Surface to human; privileged paths require human edit.
DEFERRED_HAS_TARGETclose_phase deferred item without deferred_to_milestone.Re-emit with the target milestone in a later phase (MR-2).
HUMAN_COUNTERSIGN_PRESENTclose_phase without sign_off.human.Surface to human for countersign. Do not auto-fill.
DIAGNOSIS_EXISTS / DIAGNOSIS_REPO_MATCHpropose_fix references stale or wrong-repo diagnosis.Re-run diagnose for the correct repo, retry.
PROFILE_PERMITS_AUTOMERGErequest_merge_when_green on strict-profile repo.Switch to request_review and request human approval. Do not retry same verb.
BASE_BRANCH_MISSINGRepo’s default_branch doesn’t exist.Registry data drift. Surface to human.
FILE_ALREADY_EXISTScreate 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-remote to 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_fix again 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.

Every PR body the verbs emit carries:

  • The idempotency key — re-runs are detected and become no-ops.
  • The actorfleet:<id> or human:<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).

  • 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 --apply would 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.

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_fail event.
  • The named audit agent (e.g. audit-rls) sets the actor field to fleet:<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 in mr_grounding.

This makes KAHN audits first-class events in the petrova ledger without KAHN reaching into consumer repos directly.