fix(script): provision

This commit is contained in:
null 2026-05-22 00:59:47 -05:00
parent 36025c6c67
commit fe7ce20956
5 changed files with 221 additions and 5 deletions

View File

@ -299,8 +299,10 @@ def _identity_context(agent: Agent) -> dict[str, str]:
def _role_instruction_context(agent: Agent) -> dict[str, str]:
role = _identity_context(agent).get("identity_role", "")
role_markdown = _local_role_instruction_markdown(role)
return {
"local_role_instruction_markdown": _local_role_instruction_markdown(role),
"local_role_instruction_markdown": role_markdown,
"local_role_instruction_block": _managed_role_instruction_block(role_markdown),
}
@ -406,6 +408,52 @@ def _local_role_instruction_markdown(role: str) -> str:
- Capture role-specific assumptions, risks, and verification evidence in task comments."""
def _managed_role_instruction_block(markdown: str) -> str:
markdown = markdown.strip()
if not markdown:
return ""
return f"{_ROLE_INSTRUCTION_START}\n{markdown}\n{_ROLE_INSTRUCTION_END}"
def _extract_managed_role_instruction_block(content: str) -> str:
match = _ROLE_INSTRUCTION_RE.search(content)
return match.group(0).strip() if match else ""
def _merge_managed_role_instruction_block(existing_content: str, rendered_content: str) -> str:
managed_block = _extract_managed_role_instruction_block(rendered_content)
if not managed_block:
return existing_content
if _ROLE_INSTRUCTION_RE.search(existing_content):
return _ROLE_INSTRUCTION_RE.sub(managed_block, existing_content, count=1)
return f"{existing_content.rstrip()}\n\n{managed_block}\n"
def _agent_file_content_from_payload(payload: object) -> str | None:
if isinstance(payload, str):
return payload
if not isinstance(payload, dict):
return None
content = payload.get("content")
if isinstance(content, str):
return content
file_payload = payload.get("file")
if isinstance(file_payload, dict):
content = file_payload.get("content")
if isinstance(content, str):
return content
data_payload = payload.get("data")
if isinstance(data_payload, dict):
content = data_payload.get("content")
if isinstance(content, str):
return content
return None
def _select_role_soul_ref(
refs: list[souls_directory.SoulRef],
*,
@ -958,6 +1006,25 @@ class BaseAgentLifecycleManager(ABC):
for name, content in rendered.items():
if content == "":
continue
if action in {"provision", "update"} and not overwrite and name in _ROLE_INSTRUCTION_PATCH_FILES:
entry = existing_files.get(name)
if entry and not bool(entry.get("missing")):
payload = await self._control_plane.get_agent_file_payload(
agent_id=agent_id,
name=name,
)
existing_content = _agent_file_content_from_payload(payload)
if existing_content is None:
logger.warning(
"Skipping role-instruction patch for %s:%s; gateway returned "
"unreadable file payload",
agent_id,
name,
)
continue
content = _merge_managed_role_instruction_block(existing_content, content)
if content == existing_content:
continue
# Preserve "editable" files only during updates. During first-time provisioning,
# the gateway may pre-create defaults for USER/MEMORY/etc, and we still want to
# apply Mission Control's templates.

View File

@ -9,7 +9,7 @@
- Communication Style: {{ identity_communication_style }}
- Emoji: {{ identity_emoji }}
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
{% set local_role_instructions = local_role_instruction_block | default("") | trim %}
{% if local_role_instructions %}
{{ local_role_instructions }}

View File

@ -1,5 +1,5 @@
{% set is_lead = (is_board_lead | default(false) | string | lower) in ["true", "1", "yes"] %}
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
{% set local_role_instructions = local_role_instruction_block | default("") | trim %}
{% if is_lead %}
# SOUL.md - Who You Are

View File

@ -125,6 +125,7 @@ This avoids relying on startup hooks to populate `api/openapi.json`.
- `identity_autonomy_level`, `identity_verbosity`, `identity_output_format`, `identity_update_cadence`
- `identity_purpose`, `identity_personality`, `identity_custom_instructions`
- `local_role_instruction_markdown`: deterministic local instructions derived from `identity_role`
- `local_role_instruction_block`: the same instructions wrapped in Mission Control markers so provisioning can patch existing agent files without replacing local content
### Board-agent-only keys

View File

@ -133,6 +133,7 @@ def test_rendered_agent_files_include_local_role_instructions() -> None:
name="Backend Agent",
identity_profile={"role": "Backend Code"},
)
role_markdown = agent_provisioning._local_role_instruction_markdown("Backend Code")
context = {
"agent_name": agent.name,
"agent_id": str(agent.id),
@ -145,8 +146,9 @@ def test_rendered_agent_files_include_local_role_instructions() -> None:
"is_board_lead": "false",
"directory_role_soul_markdown": "",
"directory_role_soul_source_url": "",
"local_role_instruction_markdown": agent_provisioning._local_role_instruction_markdown(
"Backend Code"
"local_role_instruction_markdown": role_markdown,
"local_role_instruction_block": agent_provisioning._managed_role_instruction_block(
role_markdown
),
}
@ -164,6 +166,34 @@ def test_rendered_agent_files_include_local_role_instructions() -> None:
assert "## Role Instructions - Backend Code" in rendered["IDENTITY.md"]
assert "## Role Instructions - Backend Code" in rendered["SOUL.md"]
assert "backend tests" in rendered["SOUL.md"]
assert "mission-control:role-instructions:start" in rendered["SOUL.md"]
def test_merge_managed_role_instruction_block_preserves_existing_content() -> None:
existing = "# SOUL.md - Who You Are\n\nCustom persona stays here.\n"
rendered = agent_provisioning._managed_role_instruction_block(
agent_provisioning._local_role_instruction_markdown("Security Review")
)
merged = agent_provisioning._merge_managed_role_instruction_block(existing, rendered)
assert "Custom persona stays here." in merged
assert "## Role Instructions - Security Review" in merged
assert "mission-control:role-instructions:start" in merged
def test_merge_managed_role_instruction_block_replaces_existing_managed_block() -> None:
old_block = agent_provisioning._managed_role_instruction_block("## Old Role\n\n- Old.")
new_block = agent_provisioning._managed_role_instruction_block(
agent_provisioning._local_role_instruction_markdown("UI")
)
existing = f"# IDENTITY.md\n\nCustom identity.\n\n{old_block}\n"
merged = agent_provisioning._merge_managed_role_instruction_block(existing, new_block)
assert "Custom identity." in merged
assert "## Role Instructions - UI" in merged
assert "## Old Role" not in merged
@dataclass
@ -449,6 +479,124 @@ async def test_set_agent_files_update_preserves_nonmissing_user_md():
assert cp.writes == []
@pytest.mark.asyncio
async def test_set_agent_files_update_patches_existing_identity_without_overwriting():
class _ControlPlaneStub:
def __init__(self):
self.writes: list[tuple[str, str]] = []
async def get_agent_file_payload(self, *, agent_id, name):
_ = (agent_id, name)
return {
"content": "# IDENTITY.md - Who Am I?\n\nCustom identity stays.\n",
}
async def set_agent_file(self, *, agent_id, name, content):
_ = agent_id
self.writes.append((name, content))
@dataclass
class _GatewayTiny:
id: UUID
name: str
url: str
token: str | None
workspace_root: str
allow_insecure_tls: bool = False
disable_device_pairing: bool = False
class _Manager(agent_provisioning.BaseAgentLifecycleManager):
def _agent_id(self, agent):
return "agent-x"
def _build_context(self, *, agent, auth_token, user, board):
return {}
gateway = _GatewayTiny(
id=uuid4(),
name="G",
url="ws://x",
token=None,
workspace_root="/tmp",
)
cp = _ControlPlaneStub()
mgr = _Manager(gateway, cp) # type: ignore[arg-type]
rendered = agent_provisioning._managed_role_instruction_block(
agent_provisioning._local_role_instruction_markdown("Code Review")
)
await mgr._set_agent_files(
agent_id="agent-x",
rendered={"IDENTITY.md": rendered},
existing_files={"IDENTITY.md": {"name": "IDENTITY.md", "missing": False}},
action="update",
)
assert len(cp.writes) == 1
name, content = cp.writes[0]
assert name == "IDENTITY.md"
assert "Custom identity stays." in content
assert "## Role Instructions - Code Review" in content
@pytest.mark.asyncio
async def test_set_agent_files_provision_patches_existing_soul_without_overwriting():
class _ControlPlaneStub:
def __init__(self):
self.writes: list[tuple[str, str]] = []
async def get_agent_file_payload(self, *, agent_id, name):
_ = (agent_id, name)
return "# SOUL.md - Who You Are\n\nCustom soul stays.\n"
async def set_agent_file(self, *, agent_id, name, content):
_ = agent_id
self.writes.append((name, content))
@dataclass
class _GatewayTiny:
id: UUID
name: str
url: str
token: str | None
workspace_root: str
allow_insecure_tls: bool = False
disable_device_pairing: bool = False
class _Manager(agent_provisioning.BaseAgentLifecycleManager):
def _agent_id(self, agent):
return "agent-x"
def _build_context(self, *, agent, auth_token, user, board):
return {}
gateway = _GatewayTiny(
id=uuid4(),
name="G",
url="ws://x",
token=None,
workspace_root="/tmp",
)
cp = _ControlPlaneStub()
mgr = _Manager(gateway, cp) # type: ignore[arg-type]
rendered = agent_provisioning._managed_role_instruction_block(
agent_provisioning._local_role_instruction_markdown("Security Review")
)
await mgr._set_agent_files(
agent_id="agent-x",
rendered={"SOUL.md": rendered},
existing_files={"SOUL.md": {"name": "SOUL.md", "missing": False}},
action="provision",
)
assert len(cp.writes) == 1
name, content = cp.writes[0]
assert name == "SOUL.md"
assert "Custom soul stays." in content
assert "## Role Instructions - Security Review" in content
@pytest.mark.asyncio
async def test_set_agent_files_update_overwrite_writes_preserved_user_md():
class _ControlPlaneStub: