hermes: close parked kanban tasks atomically

This commit is contained in:
jenkins 2026-08-16 04:31:00 -03:00
parent 7d284871e8
commit 4bab6130bc
4 changed files with 77 additions and 2 deletions

View File

@ -629,6 +629,48 @@ source = source[:runs_start] + runs_source
path.write_text(source) path.write_text(source)
PY PY
# A manual evidence-based close must not expose a parked task to the dispatcher
# between separate `unblock` and `complete` commands. Upstream already permits
# direct completion from `blocked`; extend the same atomic path to `scheduled`
# and make that contract visible in CLI help so agents do not create a ready
# window that can launch redundant work.
RUN python - <<'PY'
from pathlib import Path
db_path = Path("/opt/hermes/hermes_cli/kanban_db.py")
db_source = db_path.read_text()
state_before = ''' WHERE id = ?
AND status IN ('running', 'ready', 'blocked')
'''
state_after = ''' WHERE id = ?
AND status IN ('running', 'ready', 'blocked', 'scheduled')
'''
if db_source.count(state_before) != 2:
raise SystemExit(
"Hermes Kanban completion state gate changed: expected 2, "
f"found {db_source.count(state_before)}"
)
# Only the manual path lacks an expected run id. A worker-owned completion must
# continue to match its live run and may not revive an already scheduled run.
db_path.write_text(db_source.replace(state_before, state_after, 1))
cli_path = Path("/opt/hermes/hermes_cli/kanban.py")
cli_source = cli_path.read_text()
help_before = 'p_complete = sub.add_parser("complete", help="Mark one or more tasks done")'
help_after = (
'p_complete = sub.add_parser('
'"complete", '
'help="Atomically mark running, ready, blocked, or scheduled tasks done"'
')'
)
if cli_source.count(help_before) != 1:
raise SystemExit(
"Hermes Kanban complete help changed: expected 1, "
f"found {cli_source.count(help_before)}"
)
cli_path.write_text(cli_source.replace(help_before, help_after, 1))
PY
COPY dockerfiles/hermes-python-sandbox-tool.py /opt/hermes/tools/python_sandbox_tool.py COPY dockerfiles/hermes-python-sandbox-tool.py /opt/hermes/tools/python_sandbox_tool.py
COPY dockerfiles/hermes-public-extract/__init__.py /opt/hermes/plugins/web/public_extract/__init__.py COPY dockerfiles/hermes-public-extract/__init__.py /opt/hermes/plugins/web/public_extract/__init__.py
COPY dockerfiles/hermes-public-extract/plugin.yaml /opt/hermes/plugins/web/public_extract/plugin.yaml COPY dockerfiles/hermes-public-extract/plugin.yaml /opt/hermes/plugins/web/public_extract/plugin.yaml
@ -1409,6 +1451,10 @@ RUN cd /opt/hermes/web \
/opt/hermes/gateway/platforms/api_server.py \ /opt/hermes/gateway/platforms/api_server.py \
&& grep -Fq 'reasoning_effort=body.get("reasoning_effort")' \ && grep -Fq 'reasoning_effort=body.get("reasoning_effort")' \
/opt/hermes/gateway/platforms/api_server.py \ /opt/hermes/gateway/platforms/api_server.py \
&& grep -Fq "status IN ('running', 'ready', 'blocked', 'scheduled')" \
/opt/hermes/hermes_cli/kanban_db.py \
&& grep -Fq 'Atomically mark running, ready, blocked, or scheduled tasks done' \
/opt/hermes/hermes_cli/kanban.py \
&& grep -Fq 'routing_priority=body.get("routing_priority")' \ && grep -Fq 'routing_priority=body.get("routing_priority")' \
/opt/hermes/gateway/platforms/api_server.py \ /opt/hermes/gateway/platforms/api_server.py \
&& grep -Fq 'agent._hermes_explicit_model_pick' \ && grep -Fq 'agent._hermes_explicit_model_pick' \

View File

@ -50,6 +50,7 @@ data:
- clarify - clarify
- delegation - delegation
- file - file
- kanban
- memory - memory
- session_search - session_search
- skills - skills
@ -62,6 +63,7 @@ data:
- clarify - clarify
- delegation - delegation
- file - file
- kanban
- memory - memory
- session_search - session_search
- skills - skills
@ -246,6 +248,12 @@ data:
those findings, finish any required repair and verification, and emit the those findings, finish any required repair and verification, and emit the
task's final structured result itself. task's final structured result itself.
When authoritative evidence warrants closing a `blocked` or `scheduled`
task, call `kanban_complete` or `hermes kanban complete` directly. That
operation is atomic for parked tasks. Never unblock and then complete in
separate operations: the gateway dispatcher can claim the transient
`ready` state and launch redundant work.
For an implementation or verification task, "review-ready" is a completed For an implementation or verification task, "review-ready" is a completed
task outcome with evidence, not a reason to call `kanban_block`. Request a task outcome with evidence, not a reason to call `kanban_block`. Request a
block only for a genuine external decision or unavailable capability. A block only for a genuine external decision or unavailable capability. A

View File

@ -4,7 +4,7 @@ kind: Kustomization
namespace: hermes namespace: hermes
images: images:
- name: registry.bstein.dev/bstein/hermes-agent - name: registry.bstein.dev/bstein/hermes-agent
digest: sha256:8a4a33df755607f69bc8803181cc8d38edf65330343ec8bf94ac5e24ba8d5e58 digest: sha256:37ebf720c783ae908a602916ffccf88d43d205a157957f5dc4b487867aee45e7
resources: resources:
- namespace.yaml - namespace.yaml
- vault-serviceaccount.yaml - vault-serviceaccount.yaml

View File

@ -76,8 +76,16 @@ def test_chat_config_enables_real_research_compute_and_delegation():
def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle(): def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
configmap = _documents(HERMES / "agent-configmap.yaml")[0] configmap = _documents(HERMES / "agent-configmap.yaml")[0]
instructions = configmap["data"]["AGENTS.md"] instructions = configmap["data"]["AGENTS.md"]
config = yaml.safe_load(configmap["data"]["config.yaml"])
assert "Only the foreground durable worker owns its Kanban task lifecycle" in instructions assert (
"Only the foreground durable worker owns its Kanban task lifecycle"
in instructions
)
assert "Never unblock and then complete" in instructions
assert "gateway dispatcher can claim the transient" in instructions
for platform in ("cli", "api_server"):
assert "kanban" in config["platform_toolsets"][platform]
assert "must never complete, block, unblock, reclaim" in instructions assert "must never complete, block, unblock, reclaim" in instructions
assert "task's final structured result itself" in instructions assert "task's final structured result itself" in instructions
assert "Atlas organization has private visibility" in instructions assert "Atlas organization has private visibility" in instructions
@ -85,6 +93,19 @@ def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
assert "already supplied through `GIT_ASKPASS`" in instructions assert "already supplied through `GIT_ASKPASS`" in instructions
def test_agent_image_completes_parked_kanban_tasks_atomically():
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-agent").read_text()
assert (
"AND status IN ('running', 'ready', 'blocked', 'scheduled')"
in dockerfile
)
assert (
"Atomically mark running, ready, blocked, or scheduled tasks done"
in dockerfile
)
def test_sandbox_shares_only_the_tenant_workspace_without_credentials(): def test_sandbox_shares_only_the_tenant_workspace_without_credentials():
sandbox_docs = _documents(HERMES / "chat-sandbox.yaml") sandbox_docs = _documents(HERMES / "chat-sandbox.yaml")
deployments = [doc for doc in sandbox_docs if doc["kind"] == "Deployment"] deployments = [doc for doc in sandbox_docs if doc["kind"] == "Deployment"]