feat(orchestrator+bootstrap): KVM control surface + bootstrap-driven routing — Mavis inter-agent handoff v0 - #2651
Conversation
Added x-cgp-* annotations + x-consumer-contract block to the bootstrap schema: - x-cgp-profile: pmoves.bootstrap/v1 (this is the profile name) - x-cgp-base: link to the canonical CGP v1.0 base schema - x-cgp-spec: link to the CGP v1.0 specification doc - x-cgp-version / x-cgp-bootstrap-version: 1.0.0 - x-consumer-contract.required: - Read bootstrap ONCE at session init (not every turn) - Validate spec is exactly 'pmoves.bootstrap/v1'; refuse otherwise - Apply routing block to populate dispatch table - Honor constraints (a fork that violates is broken, not the producer) - Treat services as advisory (missing services skipped, not failed) - Never replace existing config; this is a manifest, not a config - x-consumer-contract.forbidden: - Never mutate the CGP and write it back - Never rely on services that aren't in the local env - Never bypass CHIT signing / force-push / CI bypass This gives the consumer forks (Hermes, Pinokio) an explicit machine-readable contract they can lint against. Refs: Mavis inter-agent handoff slice commit 1
…has 3 known targets - v1.schema.json routing block: added pinokio with the same shape as kiloclaw/hermes (node + nats_subject + target); additionalProperties:false is preserved so typo'd agent names still fail at schema-level - example.cgp.yaml: added a pinokio entry under routing so the example file is round-trippable - pmoves/tools/load_bootstrap.py: Routing dataclass now carries pinokio; export_env() emits PMOVES_BOOTSTRAP_TARGET_PINOKIO alongside the existing KILOCLAW / HERMES vars This unblocks the Pinokio-fork consumer wire-up (CRUSH handoff). The orchestrator derives KNOWN_TARGETS from the bootstrap's routing block, so the 3rd target appears automatically. Refs: Mavis inter-agent handoff slice commit 2
Three changes that close the consumer-fork wire-up half of PR #2477: (1) **Bootstrap-driven known_targets**: replace the hardcoded KNOWN_TARGETS set with a property that derives the set from self.bootstrap.routing + the implicit 'mavis' self-target. Adding a routing entry to the CGP (kiloclaw / hermes / pinokio / future) automatically widens the dispatch surface; no orchestrator code change needed. (2) **routing_for(target)**: returns the CGP routing entry for a target, or {} if unknown. 'mavis' returns a synthesized self-entry. Used by the KVM surface and any external consumer that needs node/target metadata for a given dispatch. (3) **publish_kvm_focus(task_id, target, node)**: the KVM control surface. dispatch() now publishes a pmoves.bpm.phase.v1 event with phase='kvm-focus', target, and target_node when a task lands on a target whose routing entry names a different node. An external KVM controller (a separate service that subscribes to phase events) can then switch the operator's focus via RustDesk + Tailscale to the named node. Local self-dispatches (mavis / host / self) are no-ops on the KVM channel so Mavis's in-session work doesn't trigger KVM switches. The KVM event is published on the existing pmoves.bpm.phase.v1 subject (with a 'kvm-focus' phase discriminator) so the KVM controller reuses the existing phase-event subscriber. Adding a new subject would have been option 2; reusing the existing one matches the harness's 'tagged-services-are-advisory' discipline. Refs: Mavis inter-agent handoff slice commit 3
…VM slice 17 tests, all pass: - 3 known_targets tests (full bootstrap / empty bootstrap / peer-with-routing-only) - 4 routing_for tests (mavis self-entry / kiloclaw CGP entry / unknown / copy-not-reference) - 7 KVM tests (kiloclaw publishes / mavis no-op / pinokio no-op / hermes / self no-op / empty no-op / remote node) - 3 regression tests for the existing dispatch envelope + error path LEARNINGS file (6 lessons + 3 follow-ups): - bootstrap-driven known_targets > hardcoded set (Lesson 1) - KVM in orchestrator > new subject (Lesson 2) - routing_for returns a copy, not a reference (Lesson 3) - x-cgp-* annotations make the consumer contract machine-checkable (Lesson 4) - KVM no-op on self/host/empty is the floor, not a special case (Lesson 5) - cross-PR coordination via AGNOTE lane board (Lesson 6) Refs: Mavis inter-agent handoff slice commit 4
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26b0609a9f
ℹ️ 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".
… peer list
All four were real, and the first two each produce a failure with no error
message: the work simply never happens.
## 1. The envelope carried the alias, not the configured target
routing.hermes.target is "hermes-3", and the Hermes handoff in
AGNOTE4482PHI.t1.md subscribes on exactly that. dispatch() published
target="hermes" (the alias), so no consumer ever matched and the handoff would
sit pending forever.
Now publishes the configured target with the alias alongside it:
{"target": "hermes-3", "target_alias": "hermes", ...}
Producer-side correlation keeps working; the consumer can finally match.
## 2. KVM focus rode a contracted subject it did not fit
.claude/context/nats-subjects.md contracts pmoves.bpm.phase.v1 as the five
lifecycle phases define -> assign -> execute -> review -> close, carrying
task_name/previous_phase. The focus event published `phase: "kvm-focus"` with a
target_node and neither of those fields, so A2UI or observability reading that
stream would see an invalid lifecycle transition.
Split to pmoves.kvm.focus.v1 and registered in nats-subjects.md. The reasoning
is inline at the constant, because the original code deliberately chose to reuse
the stream: a discriminator field does not make an incompatible payload
compatible, it moves the breakage into the consumer.
## 3. TBD is not a machine
example.cgp.yaml ships `hermes.node: TBD` -- "operator hasn't stood Hermes up
yet; wire is ready". The guard was `node not in ("self", "host", "")`, so a
literal TBD passed and a DEFAULT-config dispatch asked the KVM controller to
switch to a node named TBD.
_is_actionable_node() now rejects placeholders (tbd/todo/none/n/a/-) as well as
the local device, case-insensitively and whitespace-trimmed, because a config is
hand-written.
## 4. known_targets contradicted its own docstring
It promised that adding a routing entry widens the dispatch surface with no
orchestrator change, then enumerated a hard-coded ("kiloclaw", "hermes",
"pinokio"). A new peer would have been rejected while appearing configured. Now
derived from dataclasses.fields(Routing).
## Tests: 17 passed (was 12 passed / 3 failed)
The three failures were the pre-KVM tests asserting a single publish and
`target == "kiloclaw"`. Both were the OLD contract, so they were updated to the
requirement rather than the implementation -- kiloclaw's node is 5090, a real
remote machine, so a focus event there is correct.
Five added, each falsified against the specific defect:
revert to publishing the alias -> test_wire_target_matches_what_the_consumer_subscribes_to
drop the placeholder guard -> test_placeholder_node_does_not_request_kvm_focus
put focus back on bpm.phase.v1 -> test_kvm_focus_is_not_on_the_bpm_phase_stream
The placeholder test asserts against the DEFAULT config rather than substituting
a node, since the shipped config is what takes the placeholder path -- the
review's point that substituting `spark` masked it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d LEARNINGS to 8 lessons The 4 review findings (operator + Claude Opus 5 in commit 311093c) flipped the contract: - wire target = routing.target (was: alias) - target_alias = the routing key (was: just 'target') - KVM subject = pmoves.kvm.focus.v1 (was: bpm.phase.v1) - actionable node = rejects placeholders (was: literal self/host/'') - known_targets = derived from dataclasses.fields(routing) (was: literal tuple) The fix commit shipped a comprehensive 17-test file at pmoves/tools/tests/test_orchestrator.py that asserts the NEW contract. The old pmoves/tests/test_orchestrator_kvm.py (mine) asserts the OLD contract and is now stale; dropping it. LEARNINGS expanded from 6 to 8 lessons: - Lesson 2 expanded: KVM on its own subject, not a phase discriminator (the 'incompatible payload' principle) - NEW Lesson 4: wire target is routing.target, not the alias (P1 finding: the handoff would have sat pending forever) - NEW Lesson 5: placeholders are not machine names; case-insensitive set lookup (the 'TBD' finding) - NEW Lesson 8: pair-review catches P1 bugs the original author misses (4 of the 8 lessons are review findings, not author-self-finds) Refs: Mavis inter-agent handoff slice post-review cleanup
…, registry entry Step 1 of the Mavis-on-SPARK onboarding (config surface; operator approved): - minimax_edition.yaml: node_affinity gains spark (GB10 128GB unified — long-context overflow + edge dispatch target). Registry convention is lowercase spark; profile order becomes 5090 -> spark -> 4090 -> z890. - secrets_manifest.yaml: new minimax_token_plan_api_key entry (cgp label MINIMAX_TOKEN_PLAN_API_KEY -> .env.generated + env.tier-llm, optional like every provider key). The profile declares token_plan_api_key_env but no manifest entry carried the slot, so the funnel could never materialize the value on any node. - agent_registry.yaml: new mavis agents-block entry — the orchestrator identity of the MiniMax Edition claw (signature minimax, profile minimax_edition). NATS surface matches the #2651 harness contract: publishes pmoves.agent.task.v1 / pmoves.bpm.phase.v1, subscribes pmoves.agent.result.v1 / pmoves.bpm.pomodoro.v1. Not in this PR (deliberately): the token value itself (operator custody via the funnel), the SPARK pmoves.agent.task.v1 subscriber (the wire that makes SPARK an actual dispatch target — affinity is only a hint), and any KiloCode install decision. Verification: all three YAMLs parse; agent_registry_check.py clean (42 entries); 37 registry/profile/manifest tests pass. 2 pre-existing failures (v2 manifest 'supabase' tier vs the test's 6-tier list) are identical on clean main and belong to the #2633 lane. 💘 Generated with Crush
…, registry entry Step 1 of the Mavis-on-SPARK onboarding (config surface; operator approved): - minimax_edition.yaml: node_affinity gains spark (GB10 128GB unified — long-context overflow + edge dispatch target). Registry convention is lowercase spark; profile order becomes 5090 -> spark -> 4090 -> z890. - secrets_manifest.yaml (v1+v2): new minimax_token_plan_api_key entry (cgp label MINIMAX_TOKEN_PLAN_API_KEY -> .env.generated + env.tier-llm, optional like every provider key). The profile declares token_plan_api_key_env but no manifest entry carried the slot, so the funnel could never materialize the value on any node. - agent_registry.yaml: new mavis agents-block entry — the orchestrator identity of the MiniMax Edition claw (signature minimax, profile minimax_edition). NATS surface matches the #2651 harness contract: publishes pmoves.agent.task.v1 / pmoves.bpm.phase.v1, subscribes pmoves.agent.result.v1 / pmoves.bpm.pomodoro.v1. Not in this PR (deliberately): the token value itself (operator custody via the funnel), the SPARK pmoves.agent.task.v1 subscriber (the wire that makes SPARK an actual dispatch target — affinity is only a hint), and any KiloCode install decision. Verification: all three YAMLs parse; agent_registry_check.py clean (42 entries); 37 registry/profile/manifest tests pass. 2 pre-existing failures (v2 manifest 'supabase' tier vs the test's 6-tier list) are identical on clean main and belong to the #2633 lane. 💘 Generated with Crush
Closes onboarding steps 2-3 of the Mavis-on-SPARK lane: - pmoves/tools/agent_task_subscriber.py — the consumer half of the #2651 inter-agent harness. Subscribes pmoves.agent.task.v1, matches target or alias, runs a pluggable handler, publishes the result on pmoves.agent.result.v1 using the exact envelope Orchestrator.dispatch emits. Default handler acknowledges (stops dispatchers reporting pending forever); --handler module:callable carries real work. Hardened by the live round-trip that found two real bugs: core-NATS msg.ack() kills the callback (now JetStream-gated), and host shells cannot resolve env.shared's in-network nats hostname while nats-py retries a dead name for minutes (DNS pre-check swaps to the localhost published port before connect). Round-trip proven live on SPARK: task -> subscriber -> handler -> result, task_id match, status completed, node PMOVES-SPARK. - agent-profiles/kiloclaw.yaml — the fleet-wide KiloCode claw behind routing.kiloclaw (wire target glm-5.1). KiloCode runs on 5090 AND laptop-4090 (the Mavis session host since 2026-08), not just 5090; until now the only profile was kilocode_glm.yaml, the 5090 DARKXSIDE edition. Same shape as the other claws: affinity, model lanes, harness NATS subjects, three-body roles, exec permissions, health. - bootstrap example + schema: routing.kiloclaw now names both nodes and the subscriber command that makes a node dispatchable; schema examples/description updated to match. Tests pin all of it. 14 unit tests (matching, envelope, handler loading, subject parity with the orchestrator constants, profile/routing wiring). 💘 Generated with Crush
…, registry entry (#2693) * feat(fleet): Mavis gains a SPARK lane — affinity, token-plan key slot, registry entry Step 1 of the Mavis-on-SPARK onboarding (config surface; operator approved): - minimax_edition.yaml: node_affinity gains spark (GB10 128GB unified — long-context overflow + edge dispatch target). Registry convention is lowercase spark; profile order becomes 5090 -> spark -> 4090 -> z890. - secrets_manifest.yaml (v1+v2): new minimax_token_plan_api_key entry (cgp label MINIMAX_TOKEN_PLAN_API_KEY -> .env.generated + env.tier-llm, optional like every provider key). The profile declares token_plan_api_key_env but no manifest entry carried the slot, so the funnel could never materialize the value on any node. - agent_registry.yaml: new mavis agents-block entry — the orchestrator identity of the MiniMax Edition claw (signature minimax, profile minimax_edition). NATS surface matches the #2651 harness contract: publishes pmoves.agent.task.v1 / pmoves.bpm.phase.v1, subscribes pmoves.agent.result.v1 / pmoves.bpm.pomodoro.v1. Not in this PR (deliberately): the token value itself (operator custody via the funnel), the SPARK pmoves.agent.task.v1 subscriber (the wire that makes SPARK an actual dispatch target — affinity is only a hint), and any KiloCode install decision. Verification: all three YAMLs parse; agent_registry_check.py clean (42 entries); 37 registry/profile/manifest tests pass. 2 pre-existing failures (v2 manifest 'supabase' tier vs the test's 6-tier list) are identical on clean main and belong to the #2633 lane. 💘 Generated with Crush * feat(harness): the dispatch wire + a kiloclaw of its own Closes onboarding steps 2-3 of the Mavis-on-SPARK lane: - pmoves/tools/agent_task_subscriber.py — the consumer half of the #2651 inter-agent harness. Subscribes pmoves.agent.task.v1, matches target or alias, runs a pluggable handler, publishes the result on pmoves.agent.result.v1 using the exact envelope Orchestrator.dispatch emits. Default handler acknowledges (stops dispatchers reporting pending forever); --handler module:callable carries real work. Hardened by the live round-trip that found two real bugs: core-NATS msg.ack() kills the callback (now JetStream-gated), and host shells cannot resolve env.shared's in-network nats hostname while nats-py retries a dead name for minutes (DNS pre-check swaps to the localhost published port before connect). Round-trip proven live on SPARK: task -> subscriber -> handler -> result, task_id match, status completed, node PMOVES-SPARK. - agent-profiles/kiloclaw.yaml — the fleet-wide KiloCode claw behind routing.kiloclaw (wire target glm-5.1). KiloCode runs on 5090 AND laptop-4090 (the Mavis session host since 2026-08), not just 5090; until now the only profile was kilocode_glm.yaml, the 5090 DARKXSIDE edition. Same shape as the other claws: affinity, model lanes, harness NATS subjects, three-body roles, exec permissions, health. - bootstrap example + schema: routing.kiloclaw now names both nodes and the subscriber command that makes a node dispatchable; schema examples/description updated to match. Tests pin all of it. 14 unit tests (matching, envelope, handler loading, subject parity with the orchestrator constants, profile/routing wiring). 💘 Generated with Crush * fix(minimax): the model surface says M2.7 while sessions run MiniMax-M3 Mavis sessions have run MiniMax-M3 since 2026-08-18, but the profile and the signature both described M2.7/M2.1. Reviewed through the MiniMax-Provider-Verifier as the gate intends: - static gate 6/6 PASS (after initializing the submodule -- it fails 5/6 uninitialized, a node-side onboarding gap in itself) - the verifier's m3_format_check suite is the M3 evidence: June-2026 conformance table, identity string MiniMax-M3-taoxi, and the default model ID is exactly "MiniMax-M3" Updates: minimax_edition.yaml primary MiniMax-M3 with fallback chain m3 -> m2.7 (1M long-context) -> m2.1 (100K efficient) across model, fallback, tensorzero variants, and the agent_zero binding; agent_signatures.yaml description and model_suits carry the same chain. Live-API disclosure: the token-plan key does not authenticate against the raw chatcompletion_v2 endpoints (1004 on both known hosts) -- the Token Plan key/endpoint pairing is a separate surface whose full conformance run is the operator's canonical path per the verifier's two-piece design. The model-ID evidence stands on the verifier, not a completed live call. Also records the 2696/2697 registration-pattern review: harness registration (fork_registry + pending harness-registry schema) is distinct from agent identity (agent_registry + profile + signature); the mavis/kiloclaw entries follow the agent pattern and need no change, and kiloclaw's provider stanzas migrate to the harness registry when that lane lands. 💘 Generated with Crush * fix(minimax): endpoint host was the retired api.minimax.chat Verified against current MiniMax Token Plan docs: the plan key authenticates on api.minimax.io (international) / api.minimaxi.com (China) via the OpenAI-compatible /v1/chat/completions path with model id "MiniMax-M3" exactly as cased; the old chatcompletion_v2 path is deprecated. Both minimax_edition.yaml's endpoint and the registry MCP entry's MINIMAX_API_HOST default pointed at the retired api.minimax.chat host. The live call still 401s with the stored token-plan key even on the correct endpoint/path/model -- key custody is the operator's next step (verify or rotate the subscription key on the MiniMax platform, then re-run the full provider-verifier conformance pass as the canonical acceptance). 💘 Generated with Crush --------- Co-authored-by: Agent Zero <agent.zero@pmoves.ai>
…Mavis closure Codex P2: the register's RELEASE-pairing contract keeps closure authority with the signing owner; a co-owner cannot close another owner's claim. Rewritten: HERMES-AGENT RELEASE acknowledging #2651's delivery, the Mavis CLAIM annotated closed-by-evidence, formal Mavis RELEASE still owed to Mavis. ACK re-signed under HERMES-AGENT identity.
… note (#2983) * docs(register): BACKFILL RELEASE mavis inter-agent-handoff v0 — delivered via #2651 16 days ago, never recorded * fix(register): backfill row is HERMES-AGENT's acknowledgement, not a Mavis closure Codex P2: the register's RELEASE-pairing contract keeps closure authority with the signing owner; a co-owner cannot close another owner's claim. Rewritten: HERMES-AGENT RELEASE acknowledging #2651's delivery, the Mavis CLAIM annotated closed-by-evidence, formal Mavis RELEASE still owed to Mavis. ACK re-signed under HERMES-AGENT identity. * feat(funnel): SUPABASE_DB_URI composed entry + gateway-token mint note — evidence from prod bundle Live-measured against the 108-point prod CGP bundle (run 34062006446): - HOSTINGER_API_KEY present in bundle; manifest alias resolved it into env.tier-agent/.env.generated this session (WARN cleared at source) - MCP_GATEWAY_AUTH_TOKEN absent even in prod — never minted anywhere; documented the one-time secrets-rotate mint path - SUPABASE_DB_URI absent; added static-composed entry (user/password from CGP + compose defaults :54322, sslmode=disable per GOTRUE pattern, URL-encoded password via the urlencoded tier file) * fix(runners): custom lanes must carry ai-lab label — elder-melchor dispatch queued forever without it setup-runner.sh's custom-lane branch emitted 'self-hosted,pmoves,<lane>' but sync-secrets-local.yml runs-on requires [self-hosted, ai-lab, <target>] — an elder-melchor dispatch sat queued 45+ min with 6 runners online because none matched. Live-verified: manual registration WITH ai-lab went online and picked up the stranded job within seconds. Custom lanes now include ai-lab. --------- Co-authored-by: HERMES Agent <hermes-agent@pmoves.ai>
* docs(register): BACKFILL RELEASE mavis inter-agent-handoff v0 — delivered via #2651 16 days ago, never recorded * fix(register): backfill row is HERMES-AGENT's acknowledgement, not a Mavis closure Codex P2: the register's RELEASE-pairing contract keeps closure authority with the signing owner; a co-owner cannot close another owner's claim. Rewritten: HERMES-AGENT RELEASE acknowledging #2651's delivery, the Mavis CLAIM annotated closed-by-evidence, formal Mavis RELEASE still owed to Mavis. ACK re-signed under HERMES-AGENT identity. --------- Co-authored-by: HERMES Agent <hermes-agent@pmoves.ai>
Summary
Closes the consumer-fork wire-up half of PR #2477 (Mavis harness v0). The Mavis-side orchestrator + bpm_cron shipped in PR #2477; the missing half was the dispatcher knowing how to route to the 3 consumer forks (Hermes, KiloClaw, Pinokio) and surfacing the KVM focus-switch to the operator. This PR lands the Mavis side. Two parallel PRs (in the AGNOTE lane board) handle the consumer-fork sides:
What lands (4 commits)
outing_for(target)\ helper. Add \publish_kvm_focus(task_id, target, node)\ which publishes a \pmoves.bpm.phase.v1\ event with \phase: kvm-focus\ when a task lands on a remote-node target. \dispatch()\ calls \publish_kvm_focus\ for every target; self/host/empty are no-ops on the KVM channel.
Files
Testing
\
17 passed in 0.12s
\\
Cumulative across this PR's tests + CGP-v1.0 tests + bootstrap-env-rotate tests: 63/63 pass.
Risks
None material:
CHIT trail
Unsigned-local (no \CHIT_PASSPHRASE\ in Mavis session per established operator rule). The CLAIM+RELEASE row in AGNOTE4482PHI.t1.md documents the slice; RELEASE row will follow at merge.