diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index b36e22985a..10f160d3fd 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -727,9 +727,11 @@ For every automatic heartbeat turn, the agent-facing checklist is: Every heartbeat guard commits one idempotent receipt. If `effective_action=monitor_quiet_skip`, it also commits the no-spend stall observation and returns the follow-up decision; do not append another manual - poll. Return quiet `DONT_NOTIFY` if it remains monitor-only. Keep monitor - todos visible but do no delivery edits and no spend until material evidence - changes or the guard exposes `autonomous_replan_required` / + poll. For a monitor-only turn with `DONT_NOTIFY`, leave chat quiet. If the + host requires a completion result, use its complete quiet result without + additional chat text. A bare decision or skip explanation is not a host + result. Keep monitor todos visible and do no delivery edits or spend until + material evidence changes or the guard exposes `autonomous_replan_required` / `execution_obligation.must_attempt_work=true`. 3. For `notify_user_on_open_todo=true`, follow the current `interaction_contract.user_channel` for the concrete notice and cooldown. diff --git a/loopx/control_plane/heartbeat/rules.py b/loopx/control_plane/heartbeat/rules.py index 823e6e9129..d3e607f2c0 100644 --- a/loopx/control_plane/heartbeat/rules.py +++ b/loopx/control_plane/heartbeat/rules.py @@ -19,10 +19,14 @@ "Local outcomes never replace root continuation, budget or stop criteria." ) +HEARTBEAT_QUIET_COMPLETION_RULE = ( + "DONT_NOTIFY: no chat text. Return any required host quiet result in full." +) USER_TODO_FINAL_MESSAGE_RULE = ( f"{OPERATOR_LANGUAGE_RULE} " "`interaction_contract.user_channel.notify` controls output: " - "NOTIFY=concrete action; DONT_NOTIFY=quiet. " + "NOTIFY=concrete action. " + f"{HEARTBEAT_QUIET_COMPLETION_RULE} " "`should_run`/due monitor/other-agent todos are not user prompts. " "Only under NOTIFY, `action_required` without an action: say the specific " "user Todo is not projected and repair LoopX state projection. Under " @@ -31,7 +35,7 @@ HEARTBEAT_NOTIFICATION_RULE_SHORT = ( f"{OPERATOR_LANGUAGE_RULE_THIN} " "`user_channel.notify` controls OUTPUT only: NOTIFY=show; " - "DONT_NOTIFY=no output. " + f"{HEARTBEAT_QUIET_COMPLETION_RULE} " "`heartbeat_recommendation.agent_must_attempt`/" "`execution_obligation.must_attempt_work`: true=work+writeback; only " "false permits no-op. Due/peer work is not a user prompt. Missing NOTIFY " diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index 7ef45d1309..e7c4f7473d 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -10,6 +10,7 @@ DEFAULT_MATERIAL_QUEUE_RULE, DEFAULT_PERMISSION_RULE, HEARTBEAT_NOTIFICATION_RULE_SHORT, + HEARTBEAT_QUIET_COMPLETION_RULE, HEARTBEAT_TURN_BOOTSTRAP_RULE, HEARTBEAT_KNOWN_CHOICE_RULE, HEARTBEAT_VISION_WRITEBACK_RULE_SHORT, @@ -161,8 +162,7 @@ def render_heartbeat_task_body( Still do not launch/stop/restart/sync/design code or mutate production unless `should_run=true` or the user explicitly authorizes it. - Otherwise, do not do implementation work, adapter work, file edits, research, - or project exploration in this turn. Return a quiet heartbeat `DONT_NOTIFY` - response with the skip reason. + or project exploration in this turn. Follow the user-channel rule above. If the result says `should_run=true`: @@ -367,12 +367,13 @@ def render_compact_heartbeat_task_body( `agent_read_required`: drain/read/triage before work; settle/ACK. Output policy: authority=`interaction_contract.user_channel.notify`; -external=`NOTIFY`; quiet=`DONT_NOTIFY`; quiet_missing_action=`internal_repair`. +external=`NOTIFY`; quiet_missing_action=`internal_repair`. +{HEARTBEAT_QUIET_COMPLETION_RULE} If `should_run=false`: `monitor_quiet_skip` -> receipt/stall; quiet unless replan; failed write -> retry id; no edits/spend; receipts do not self-stop. Only under `NOTIFY`, `state=operator_gate`/`notify_user_on_open_todo=true` -permit concrete blocker-push; else quiet. +permit concrete blocker-push. Honor repeat/cooldown. `safe_bypass_allowed=true`: validate/settle its allowed work. Wait: one allowed read-only poll; unchanged quiet, new evidence follows the current monitor/auxiliary contract. @@ -680,9 +681,10 @@ def render_thin_heartbeat_task_body( {HOST_LOOP_SAFETY_RULE} {scope_sentence} -Use TurnEnvelope: `action.must_attempt` requires work; `user.notify` controls -output only. ok=false -> recovery, no delivery. Language=user. Honor `boundary`, `execution_policy`, `contract_capsule`, -`work_context`, remaining `required_reads`, `agent_context`, and `replan_action_packet` when present. +Use TurnEnvelope: `action.must_attempt`=work; `user.notify`=output only. +{HEARTBEAT_QUIET_COMPLETION_RULE} +ok=false: recover, no delivery. Language=user. Honor `boundary`, `execution_policy`, `contract_capsule`, +`work_context`, remaining `required_reads` and `replan_action_packet` when present. When present, read inline `agent_context` before planning. Read `detail_ref.full_decision` for selection, replan, missing/truncated commands or an `agent_context.detail_ref`: same-invocation observation, not fresh admission. diff --git a/tests/control_plane/test_heartbeat_agent_input.py b/tests/control_plane/test_heartbeat_agent_input.py index a37ba7eed7..34940becdf 100644 --- a/tests/control_plane/test_heartbeat_agent_input.py +++ b/tests/control_plane/test_heartbeat_agent_input.py @@ -71,6 +71,8 @@ def test_thin_cli_preserves_readable_admission_and_authority_with_host_scope(tmp body = packet["task_body"] assert "Equal peer `worker-a` (peer_v1)" in body assert f"scope: {scope}" in body + assert "DONT_NOTIFY: no chat text" in body + assert "required host quiet result in full" in body assert "quota claim/lease and workspace contract plus repository rules" in body assert "Follow todo continuation policy" in body assert "Task-scoped coordination grants no authority over other agents" in body diff --git a/tests/control_plane/test_heartbeat_notification_rule.py b/tests/control_plane/test_heartbeat_notification_rule.py index e3e47ecca2..a97c336a59 100644 --- a/tests/control_plane/test_heartbeat_notification_rule.py +++ b/tests/control_plane/test_heartbeat_notification_rule.py @@ -94,6 +94,23 @@ def test_rendered_task_bodies_keep_execution_obligation_authority() -> None: assert "Missing vision: same-turn checkpoint-context" not in body +def test_codex_app_quiet_completion_is_host_control_not_chat(tmp_path) -> None: + variants = [{mode: True} for mode in ("full", "compact", "brief", "thin")] + variants.append({ + "thin": True, "turn_envelope": True, + "decision_output_root": tmp_path, "turn_instance_id": "fixture-quiet-turn", + }) + for variant in variants: + body = build_heartbeat_prompt( + goal_id="quiet-completion-fixture", agent_id="worker-a", + registered_agents=["worker-a"], runtime_profile="codex_app_heartbeat", + **variant, + )["task_body"] + assert "DONT_NOTIFY: no chat text" in body, variant + assert "required host quiet result in full" in body, variant + assert "response with the skip reason" not in body, variant + + def test_all_heartbeat_modes_scope_no_spend_to_observation() -> None: # Build the shipped payload, including the short/default worker entrypoint. # An unchanged artifact may still accompany a validated route elimination;