diff --git a/deploy/auto-deploy.sh b/deploy/auto-deploy.sh index f191303..e9002e7 100755 --- a/deploy/auto-deploy.sh +++ b/deploy/auto-deploy.sh @@ -131,7 +131,7 @@ if [ "$OLD_SHA" = "none" ] || git diff --name-only "$OLD_SHA" "$NEW_SHA" -- syst fi fi -# Restart services only if Python files changed +# Restart services only when changed files affect a running process. RESTART="" add_restart() { case " $RESTART " in @@ -164,7 +164,8 @@ if [ "$OLD_SHA" != "none" ]; then if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- diagnostics/ 2>/dev/null | grep -q '\.py$'; then add_restart teleo-diagnostics fi - if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/ 2>/dev/null | grep -q .; then + if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/ 2>/dev/null \ + | bash deploy/leoclean-gateway-restart-required.sh; then add_restart_if_unit_active leoclean-gateway fi else @@ -216,7 +217,7 @@ if [ -n "$RESTART" ]; then exit 1 fi else - log "No Python changes — services not restarted" + log "No runtime changes - services not restarted" fi echo "$NEW_SHA" > "$STAMP_FILE" diff --git a/deploy/leoclean-gateway-restart-required.sh b/deploy/leoclean-gateway-restart-required.sh new file mode 100644 index 0000000..fdcae0f --- /dev/null +++ b/deploy/leoclean-gateway-restart-required.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read changed repo paths from stdin. Markdown skills are loaded from the synced +# profile per turn; runtime code changes still require a gateway restart. +restart_required=1 +while IFS= read -r path || [ -n "$path" ]; do + case "$path" in + hermes-agent/leoclean-bin/*) + restart_required=0 + ;; + hermes-agent/leoclean-skills/vps/*.md|hermes-agent/leoclean-skills/vps/*/*.md) + ;; + hermes-agent/leoclean-skills/vps/*) + restart_required=0 + ;; + esac +done + +exit "$restart_required" diff --git a/hermes-agent/leoclean-skills/gcp/teleo-kb-bridge/SKILL.md b/hermes-agent/leoclean-skills/gcp/teleo-kb-bridge/SKILL.md index e61c469..43f63d9 100644 --- a/hermes-agent/leoclean-skills/gcp/teleo-kb-bridge/SKILL.md +++ b/hermes-agent/leoclean-skills/gcp/teleo-kb-bridge/SKILL.md @@ -152,6 +152,22 @@ and copy either (a) a full UUID plus observed `status` and `applied_at`, or (b) exact observed counts for the relevant `claims`, `sources`, `claim_edges`, `claim_evidence`, and `kb_proposals` tables. Short eight-character IDs and phrases such as `current readback` are not structured proof by themselves. +Copy exactly one of these formats, replacing every all-caps token with a value +from the current tool call: + +```text +DB readback: proposal: `PROPOSAL_UUID_36_CHARS`; status: `OBSERVED_STATUS`; applied_at: `OBSERVED_TIMESTAMP_OR_NONE`. +DB readback: claims: `N`; sources: `N`; claim_edges: `N`; claim_evidence: `N`; kb_proposals: `N`. +``` + +Never shorten a UUID: it must contain all 36 characters and four hyphens. Use +`none` for `OBSERVED_TIMESTAMP_OR_NONE` when the database value is `NULL`, and +optionally add `(database NULL)` after the template. Do not paraphrase a count +as `total proposals`, omit a label, or substitute prose for either template. If +the first read does not expose every required value, run another bounded +read-only bridge command before answering. Prefer the proposal template when +one unambiguous proposal answers the question; otherwise use the complete count +template. Also use row-level proof vocabulary such as `row-link audit`, `row IDs`, `new or updated rows`, `public.*`, and `postflight proof` where relevant. End with exactly one final line beginning diff --git a/hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md b/hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md index cb00e11..54be6b1 100644 --- a/hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md +++ b/hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md @@ -145,6 +145,22 @@ and copy either (a) a full UUID plus observed `status` and `applied_at`, or (b) exact observed counts for the relevant `claims`, `sources`, `claim_edges`, `claim_evidence`, and `kb_proposals` tables. Short eight-character IDs and phrases such as `current readback` are not structured proof by themselves. +Copy exactly one of these formats, replacing every all-caps token with a value +from the current tool call: + +```text +DB readback: proposal: `PROPOSAL_UUID_36_CHARS`; status: `OBSERVED_STATUS`; applied_at: `OBSERVED_TIMESTAMP_OR_NONE`. +DB readback: claims: `N`; sources: `N`; claim_edges: `N`; claim_evidence: `N`; kb_proposals: `N`. +``` + +Never shorten a UUID: it must contain all 36 characters and four hyphens. Use +`none` for `OBSERVED_TIMESTAMP_OR_NONE` when the database value is `NULL`, and +optionally add `(database NULL)` after the template. Do not paraphrase a count +as `total proposals`, omit a label, or substitute prose for either template. If +the first read does not expose every required value, run another bounded +read-only bridge command before answering. Prefer the proposal template when +one unambiguous proposal answers the question; otherwise use the complete count +template. Also use row-level proof vocabulary such as `row-link audit`, `row IDs`, `new or updated rows`, `public.*`, and `postflight proof` where relevant. End with exactly one final line beginning diff --git a/tests/test_hermes_leoclean_skill_surfaces.py b/tests/test_hermes_leoclean_skill_surfaces.py index 4c25f0c..5797da1 100644 --- a/tests/test_hermes_leoclean_skill_surfaces.py +++ b/tests/test_hermes_leoclean_skill_surfaces.py @@ -32,6 +32,12 @@ def test_gcp_kb_skill_uses_cloudsql_bridge_not_vps_docker() -> None: assert "DB readback:" in text assert "full UUID plus observed `status` and `applied_at`" in squashed assert "Short eight-character IDs" in text + assert "PROPOSAL_UUID_36_CHARS" in text + assert "OBSERVED_STATUS" in text + assert "OBSERVED_TIMESTAMP_OR_NONE" in text + assert "claims: `N`; sources: `N`; claim_edges: `N`; claim_evidence: `N`; kb_proposals: `N`" in text + assert "Never shorten a UUID" in text + assert "Do not paraphrase a count" in text assert "canonical identity requires DB rows plus" in squashed assert "not canonical Postgres" in text assert "not the source of truth" in text @@ -77,6 +83,12 @@ def test_vps_kb_skill_keeps_vps_scope_explicit() -> None: assert "DB readback:" in text assert "full UUID plus observed `status` and `applied_at`" in squashed assert "Short eight-character IDs" in text + assert "PROPOSAL_UUID_36_CHARS" in text + assert "OBSERVED_STATUS" in text + assert "OBSERVED_TIMESTAMP_OR_NONE" in text + assert "claims: `N`; sources: `N`; claim_edges: `N`; claim_evidence: `N`; kb_proposals: `N`" in text + assert "Never shorten a UUID" in text + assert "Do not paraphrase a count" in text assert "canonical identity requires DB rows plus" in squashed assert "not canonical Postgres" in text assert "not the source of truth" in text diff --git a/tests/test_teleo_agent_systemd.py b/tests/test_teleo_agent_systemd.py index f578231..cd65b62 100644 --- a/tests/test_teleo_agent_systemd.py +++ b/tests/test_teleo_agent_systemd.py @@ -1,3 +1,4 @@ +import subprocess from pathlib import Path @@ -80,7 +81,19 @@ def test_deploy_scripts_install_systemd_units_and_enable_agent_healthcheck(): assert "teleo-agent-healthcheck.timer" in manual_deploy -def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_when_active(): +def _leoclean_gateway_restart_required(*changed_paths: str) -> bool: + result = subprocess.run( + ["bash", str(REPO_ROOT / "deploy" / "leoclean-gateway-restart-required.sh")], + input="\n".join(changed_paths), + capture_output=True, + check=False, + text=True, + ) + assert result.returncode in {0, 1}, result.stderr + return result.returncode == 0 + + +def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_changes(): auto_deploy = (REPO_ROOT / "deploy" / "auto-deploy.sh").read_text() manual_deploy = (REPO_ROOT / "deploy" / "deploy.sh").read_text() sudoers = (REPO_ROOT / "deploy" / "sudoers" / "teleo-auto-deploy").read_text() @@ -89,7 +102,7 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_when_active(): assert 'LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"' in auto_deploy assert 'hermes-agent/leoclean-bin/' in auto_deploy assert 'hermes-agent/leoclean-skills/vps/' in auto_deploy - assert 'git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/' in auto_deploy + assert 'deploy/leoclean-gateway-restart-required.sh' in auto_deploy assert 'add_restart_if_unit_active leoclean-gateway' in auto_deploy assert 'sudo systemctl restart "$svc"' in auto_deploy assert "sudo systemctl restart $RESTART" not in auto_deploy @@ -102,6 +115,22 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_when_active(): assert 'systemctl is-active --quiet leoclean-gateway.service' in manual_deploy +def test_auto_deploy_hot_syncs_leoclean_markdown_without_gateway_restart(): + assert not _leoclean_gateway_restart_required( + "hermes-agent/leoclean-skills/vps/teleo-kb-bridge/SKILL.md" + ) + assert not _leoclean_gateway_restart_required( + "hermes-agent/leoclean-skills/vps/live-leo-telegram/SKILL.md" + ) + + +def test_auto_deploy_restarts_gateway_for_leoclean_runtime_changes(): + assert _leoclean_gateway_restart_required("hermes-agent/leoclean-bin/kb_tool.py") + assert _leoclean_gateway_restart_required( + "hermes-agent/leoclean-skills/vps/teleo-kb-bridge/runtime_helper.py" + ) + + def test_auto_deploy_sudoers_installer_is_root_only_and_visudo_checked(): installer = (REPO_ROOT / "deploy" / "install-auto-deploy-sudoers.sh").read_text()