Skip to content

Commit fdf05a3

Browse files
authored
fix(onboarding): qualify custom agent skill delivery (#2527)
Preserve strict Codex skill diagnostics while making custom-agent skill delivery host-owned, required for workflow readiness, and readback-verifiable.
1 parent 4726549 commit fdf05a3

6 files changed

Lines changed: 332 additions & 43 deletions

File tree

examples/control_plane/agent-onboard-host-loop-activation-smoke.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@
2929
)
3030

3131

32-
def run_cli(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
32+
def run_cli(
33+
*args: str,
34+
check: bool = True,
35+
env: dict[str, str] | None = None,
36+
) -> subprocess.CompletedProcess[str]:
3337
return subprocess.run(
3438
[sys.executable, "-m", "loopx.cli", "--format", "json", *args],
3539
cwd=REPO_ROOT,
3640
check=check,
3741
text=True,
3842
capture_output=True,
43+
env=env,
3944
)
4045

4146

@@ -301,6 +306,71 @@ def main() -> int:
301306
"loopx_goal_activate"
302307
)
303308

309+
other_agent_onboarding = build_agent_onboarding_packet(
310+
project=project,
311+
agent_type="other-agent",
312+
goal_id="multi-agent-goal",
313+
agent_id="codex-product-capability",
314+
cli_bin=cli_bin,
315+
)
316+
other_commands = other_agent_onboarding["commands"]
317+
assert "install_command_facade" not in other_commands
318+
assert "doctor --agent-type other-agent" in other_commands["doctor_or_install"]
319+
delivery = other_agent_onboarding["skill_delivery"]
320+
assert delivery["mode"] == "host_managed", delivery
321+
assert delivery["owner"] == "custom_agent_host", delivery
322+
assert delivery["status"] == "pending_host_readback", delivery
323+
assert delivery["codex_skills_root_required"] is False, delivery
324+
assert delivery["required_for_cli_health"] is False, delivery
325+
assert delivery["required_for_loopx_workflow"] is True, delivery
326+
assert set(delivery["required_skill_ids"]) == {
327+
"loopx-project",
328+
"loopx-pr-review",
329+
"loopx-doc-registry",
330+
"loopx-self-repair",
331+
}, delivery
332+
assert delivery["delivery_options"] == [
333+
"host_skill_manifest",
334+
"prompt_injection",
335+
], delivery
336+
assert delivery["source_directories"] == [
337+
f"skills/{skill_id}" for skill_id in delivery["required_skill_ids"]
338+
], delivery
339+
assert delivery["host_readback_required"] is True, delivery
340+
assert set(delivery["readback_fields"]) == {
341+
"integration_mode",
342+
"loaded_skill_ids",
343+
"source_revision",
344+
}, delivery
345+
assert any(
346+
"loaded-skill readback" in item
347+
for item in other_agent_onboarding["slash_command_contract"][
348+
"setup_complete_requires"
349+
]
350+
), other_agent_onboarding
351+
352+
doctor_home = root / "doctor-home"
353+
doctor_home.mkdir()
354+
doctor_env = {
355+
**os.environ,
356+
"HOME": str(doctor_home),
357+
"PATH": f"{REPO_ROOT / 'scripts'}{os.pathsep}{os.environ.get('PATH', '')}",
358+
}
359+
other_agent_doctor = json.loads(
360+
run_cli(
361+
"doctor",
362+
"--agent-type",
363+
"other-agent",
364+
env=doctor_env,
365+
).stdout
366+
)
367+
assert other_agent_doctor["install_freshness"]["requires_upgrade"] is False
368+
assert other_agent_doctor["skill_delivery"]["mode"] == "host_managed"
369+
for check in other_agent_doctor["checks"]:
370+
if str(check.get("id", "")).startswith("installed_"):
371+
assert check["ok"] is True, check
372+
assert check["applicable"] is False, check
373+
304374
return 0
305375

306376

loopx/agent_onboarding.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020

2121

2222
SCHEMA_VERSION = "loopx_agent_onboarding_v0"
23+
HOST_SKILL_DELIVERY_SCHEMA_VERSION = "loopx_host_skill_delivery_v0"
24+
REQUIRED_HOST_SKILL_IDS = [
25+
"loopx-project",
26+
"loopx-pr-review",
27+
"loopx-doc-registry",
28+
"loopx-self-repair",
29+
]
2330

2431

2532
def _surface_install_command(agent_type: str, cli_bin: str) -> str | None:
@@ -35,6 +42,52 @@ def _surface_install_command(agent_type: str, cli_bin: str) -> str | None:
3542
return None
3643

3744

45+
def _skill_delivery_contract(agent_type: str) -> dict[str, Any]:
46+
if agent_type != "other-agent":
47+
return {
48+
"schema_version": HOST_SKILL_DELIVERY_SCHEMA_VERSION,
49+
"mode": "surface_managed",
50+
"owner": "loopx_surface_installer",
51+
"codex_skills_root_required": agent_type.startswith("codex-"),
52+
"host_readback_required": False,
53+
}
54+
return {
55+
"schema_version": HOST_SKILL_DELIVERY_SCHEMA_VERSION,
56+
"mode": "host_managed",
57+
"owner": "custom_agent_host",
58+
"status": "pending_host_readback",
59+
"codex_skills_root_required": False,
60+
"required_for_cli_health": False,
61+
"required_for_loopx_workflow": True,
62+
"required_skill_ids": REQUIRED_HOST_SKILL_IDS,
63+
"delivery_options": [
64+
"host_skill_manifest",
65+
"prompt_injection",
66+
],
67+
"source_repository": "https://github.com/huangruiteng/loopx",
68+
"source_directories": [
69+
f"skills/{skill_id}" for skill_id in REQUIRED_HOST_SKILL_IDS
70+
],
71+
"source_contract": (
72+
"Use the same LoopX release or repository revision as the CLI when "
73+
"materializing skills or injecting equivalent SKILL.md instructions "
74+
"through the custom host."
75+
),
76+
"host_readback_required": True,
77+
"readback_fields": [
78+
"integration_mode",
79+
"loaded_skill_ids",
80+
"source_revision",
81+
],
82+
"success_criteria": [
83+
"the host reports which integration mode it selected",
84+
"the host reports every required LoopX skill id as loaded",
85+
"the host reports the source revision or digest used for delivery",
86+
"no Codex-specific skill directory is assumed",
87+
],
88+
}
89+
90+
3891
def _bootstrap_pack_command(
3992
*,
4093
project: str,
@@ -86,7 +139,11 @@ def _start_instruction(agent_type: str) -> str:
86139
return "Run `/loopx <task>`; after todo writeback, call `loopx_goal_activate` with the generated heartbeat task body."
87140
if agent_type == "manual":
88141
return "Use the CLI packet and wire an external scheduler, or run quota/status/todo commands manually."
89-
return "Use the host's explicit LoopX command facade such as `@loopx <task>` or `$loopx <task>`, then wire its scheduler through this packet."
142+
return (
143+
"Deliver the required LoopX skills through the custom host and verify its "
144+
"loaded-skill readback; then use the host's explicit LoopX command facade "
145+
"and wire its scheduler through this packet."
146+
)
90147

91148

92149
def build_agent_onboarding_packet(
@@ -132,7 +189,10 @@ def build_agent_onboarding_packet(
132189
available_capabilities=normalized_available_capabilities,
133190
)
134191
commands: dict[str, Any] = {
135-
"doctor_or_install": render_codex_cli_no_clone_preflight(cli_bin=cli_bin),
192+
"doctor_or_install": render_codex_cli_no_clone_preflight(
193+
cli_bin=cli_bin,
194+
doctor_agent_type=canonical_agent_type,
195+
),
136196
"bootstrap_command_pack": bootstrap_pack_command,
137197
"quota_guard": (
138198
render_quota_guard_command(
@@ -184,6 +244,7 @@ def build_agent_onboarding_packet(
184244
"task_text": task_text,
185245
"project_connection": inspection,
186246
"host_loop_activation": host_loop_activation,
247+
"skill_delivery": _skill_delivery_contract(canonical_agent_type),
187248
"recommended_start": (
188249
"Select one registered agent lane from identity_selection_gate, then rerun onboarding."
189250
if not activation_allowed
@@ -207,6 +268,13 @@ def build_agent_onboarding_packet(
207268
"setup_complete_requires": [
208269
"project-local LoopX state exists or was intentionally previewed",
209270
"ordered todos are written when task text was supplied",
271+
*(
272+
[
273+
"custom host loaded-skill readback satisfies skill_delivery.success_criteria"
274+
]
275+
if canonical_agent_type == "other-agent"
276+
else []
277+
),
210278
"host_loop_activation success criteria are satisfied or a concrete gate is reported",
211279
],
212280
},
@@ -230,6 +298,11 @@ def render_agent_onboarding_markdown(payload: dict[str, Any]) -> str:
230298
)
231299
identity_gate = payload.get("identity_selection_gate")
232300
identity_gate = identity_gate if isinstance(identity_gate, dict) else {}
301+
skill_delivery = (
302+
payload.get("skill_delivery")
303+
if isinstance(payload.get("skill_delivery"), dict)
304+
else {}
305+
)
233306
lines = [
234307
"# LoopX Agent Onboarding",
235308
"",
@@ -247,6 +320,24 @@ def render_agent_onboarding_markdown(payload: dict[str, Any]) -> str:
247320
]
248321
if commands.get("install_command_facade"):
249322
lines.extend(["", "```bash", str(commands.get("install_command_facade")), "```"])
323+
if skill_delivery.get("mode") == "host_managed":
324+
lines.extend(
325+
[
326+
"",
327+
"## Host-Managed Skill Delivery",
328+
"",
329+
f"- owner: `{skill_delivery.get('owner')}`",
330+
f"- required_for_cli_health: `{skill_delivery.get('required_for_cli_health')}`",
331+
f"- required_for_loopx_workflow: `{skill_delivery.get('required_for_loopx_workflow')}`",
332+
f"- delivery_options: `{','.join(skill_delivery.get('delivery_options') or [])}`",
333+
f"- required_skill_ids: `{','.join(skill_delivery.get('required_skill_ids') or [])}`",
334+
f"- source_repository: `{skill_delivery.get('source_repository')}`",
335+
f"- source_directories: `{','.join(skill_delivery.get('source_directories') or [])}`",
336+
f"- host_readback_required: `{skill_delivery.get('host_readback_required')}`",
337+
f"- readback_fields: `{','.join(skill_delivery.get('readback_fields') or [])}`",
338+
f"- source_contract: {skill_delivery.get('source_contract')}",
339+
]
340+
)
250341
lines.extend(["", "```bash", str(commands.get("bootstrap_command_pack") or ""), "```"])
251342
if commands.get("codex_cli_bootstrap_message"):
252343
lines.extend(["", "```bash", str(commands.get("codex_cli_bootstrap_message")), "```"])

loopx/cli_commands/doctor.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Callable
55

66
from ..doctor import collect_doctor, render_doctor_markdown
7+
from ..host_loop_activation import SUPPORTED_AGENT_TYPES
78

89

910
PrintPayload = Callable[
@@ -22,10 +23,21 @@ def register_doctor_command(subparsers: argparse._SubParsersAction) -> argparse.
2223
action="store_true",
2324
help="Run slower representative release-candidate checks.",
2425
)
26+
parser.add_argument(
27+
"--agent-type",
28+
choices=SUPPORTED_AGENT_TYPES,
29+
help=(
30+
"Evaluate host-specific integration checks. For other-agent, custom-host "
31+
"skill delivery replaces the Codex skill-directory check."
32+
),
33+
)
2534
return parser
2635

2736

2837
def handle_doctor_command(args: argparse.Namespace, print_payload: PrintPayload) -> int:
29-
payload = collect_doctor(deep=bool(args.deep))
38+
payload = collect_doctor(
39+
deep=bool(args.deep),
40+
agent_type=args.agent_type,
41+
)
3042
print_payload(payload, args.format, render_doctor_markdown)
3143
return 0 if payload.get("ok") else 1

0 commit comments

Comments
 (0)