From 03768b0bffe9093b5fdf339b5fb42818dd682b08 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 10 Sep 2026 19:28:35 +0800 Subject: [PATCH 1/3] fix(heartbeat): clarify scope-bounded work without tiny-step bias Signed-off-by: huangruiteng --- docs/heartbeat-automation-prompt.md | 13 +++---- docs/quota-allocation.md | 20 ++++++++--- .../control_plane/heartbeat-prompt-smoke.py | 28 ++++++++------- ...beat-recommendation-state-machine-smoke.py | 2 +- loopx/control_plane/goals/start_contract.py | 8 ++--- loopx/control_plane/heartbeat/builder.py | 6 ++-- loopx/control_plane/heartbeat/rules.py | 11 +++--- loopx/control_plane/heartbeat/task_body.py | 36 +++++++++---------- .../quota/heartbeat_recommendation.py | 8 ++--- .../work_items/interaction_contract.py | 4 +-- .../work_items/primary_action.py | 4 +-- skills/loopx-project/SKILL.md | 9 +++-- skills/loopx-self-repair/SKILL.md | 9 ++--- .../test_fine_grained_turn_mode.py | 3 ++ .../test_heartbeat_prompt_support.py | 25 +++++++++++++ .../test_heartbeat_recommendation_rules.py | 5 +-- tests/test_host_loop_activation.py | 2 +- 17 files changed, 122 insertions(+), 71 deletions(-) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 8b0af57875..8adc2f5f46 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -446,18 +446,19 @@ If the result says should_run=true: turns, do not append a quota spend for the self-cancel turn, and return NOTIFY explaining that the automation was cancelled because it was spinning without progress. -4. Choose one bounded, verifiable progress segment from that audit. It may be a - coherent batch across related implementation, test, doc, and state-writeback - files when the write scope is clear and validation is explicit; it should not - be forced into a tiny single-file step. -5. Do that segment only. Stay inside goal_boundary when present and keep +4. Choose scope-bounded work toward a verifiable result. Size by task, evidence + and risk, not calls, files or wake cadence. Related implementation, research, + tests, docs and writeback may form one coherent effort; a focused correction + can also be sufficient. One operation/writeback is not a stop condition; + obey budgets, explicit stops, settlement and replan requirements. +5. Execute that scoped work. Stay inside goal_boundary when present and keep public/private boundaries intact. Public-safe repo publication is not an operator gate by itself: for routine public project work, commit, push, and PR creation may proceed autonomously after validation and a clean public/private boundary scan. Stop and surface a user/controller gate only for private or company-internal material, credentials, destructive git operations, production actions, or repository rules that explicitly require review. -6. Run the smallest useful validation. +6. Run validation proportionate to the change and risk. 7. Write back changed files, validation, critic, and next action to the active state. If a user/owner todo appears, do not hide it in prose: `loopx todo add --goal-id --role user --task-class user_gate --blocks-agent ` diff --git a/docs/quota-allocation.md b/docs/quota-allocation.md index 5ec1060952..485eb8613d 100644 --- a/docs/quota-allocation.md +++ b/docs/quota-allocation.md @@ -992,10 +992,22 @@ common modes are: - `mapped_noop_if_unchanged`: the latest compact read-only map already exists; if there is no new user instruction, owner evidence, agent todo, stale source, or safe handoff, return a quiet no-op without another dry-run or quota spend. -- `steering_audit_then_one_step`: the goal is eligible but needs the normal - steering audit before selecting one bounded progress segment. A coherent - implementation/test/state batch is valid when scope and validation are clear; - the contract is bounded, not tiny. +- `steering_audit_then_one_step`: this compatibility mode name does not impose + a one-operation limit. The eligible goal needs a steering audit before + scope-bounded work toward a verifiable result. Work size follows task, + evidence and risk, not calls, files or wake cadence. Related implementation, + research, tests and writeback may form one coherent effort; a focused + correction can also be sufficient. Explicit stops, budgets, Todo/lease + boundaries, settlement and replan requirements remain authoritative. + +All heartbeat prompt sizes, including the default thin prompt, carry this +scope-bounded work guidance. This is a prompt clarification, not a change to +execution-profile thresholds, scheduler cadence, permissions or spend rules. +Fine-grained mode still plans independently verifiable checkpoints and checks +fresh evidence before a successor; checkpoint granularity is not a tool-call +limit. Read-only monitor polls, bounded recovery attempts and explicit +`end_current_heartbeat` transitions retain their existing limits. Do not turn +this guidance into a minimum batch size or a reason to bypass those limits. The same response includes `interaction_contract.schema_version = loopx_interaction_contract_v0`, the top-level user/agent/CLI protocol diff --git a/examples/control_plane/heartbeat-prompt-smoke.py b/examples/control_plane/heartbeat-prompt-smoke.py index 77dcca5f41..f881d08867 100644 --- a/examples/control_plane/heartbeat-prompt-smoke.py +++ b/examples/control_plane/heartbeat-prompt-smoke.py @@ -455,8 +455,8 @@ def main() -> int: "else quiet." ) in compact_task for phrase in ( - "compact LoopX heartbeat body", - "Expanded lifecycle contract", + "Compact policy: registry/state/adapter/`goal_boundary`", + "Detail:", "loopx heartbeat-prompt --full --goal-id public-heartbeat-goal --active-state /tmp/public-heartbeat-goal/ACTIVE_GOAL_STATE.md", 'loopx --format json --registry "$HOME/.codex/loopx/registry.global.json" quota should-run --goal-id public-heartbeat-goal', "state=operator_gate", @@ -629,7 +629,9 @@ def main() -> int: assert brief_payload["thin"] is False, brief_payload assert brief_payload["quota_guard_command"] == payload["quota_guard_command"], brief_payload assert brief_payload["quota_spend_command"] == payload["quota_spend_command"], brief_payload - assert len(str(brief_payload["task_body"])) < len(str(compact_payload["task_body"])) * 0.56, ( + # Preserve size ordering and the absolute budgets checked above. Essential + # shared guidance need not shrink by an arbitrary percentage in each mode. + assert len(str(brief_payload["task_body"])) < len(str(compact_payload["task_body"])), ( len(str(brief_payload["task_body"])), len(str(compact_payload["task_body"])), ) @@ -655,7 +657,7 @@ def main() -> int: "review-packet --handoff-only", "heartbeat_recommendation", "goal_boundary", - "bounded segment/batch", + "scope-bounded work", "validate/writeback/todos", "Progress(actual,no upgrade)", "Spend once; no pipe/retry", @@ -671,7 +673,7 @@ def main() -> int: "loopx heartbeat-prompt --thin --goal-id public-heartbeat-goal " "--active-state /tmp/public-heartbeat-goal/ACTIVE_GOAL_STATE.md" ), thin_payload - assert len(str(thin_payload["task_body"])) < len(str(brief_payload["task_body"])) * 0.45, ( + assert len(str(thin_payload["task_body"])) < len(str(brief_payload["task_body"])), ( len(str(thin_payload["task_body"])), len(str(brief_payload["task_body"])), ) @@ -814,15 +816,15 @@ def main() -> int: "2 consecutive eligible heartbeats are no-progress loops", "self-cancel turn", "repair path is", - "Choose one bounded, verifiable progress segment from that audit", - "coherent batch across related implementation, test, doc, and state-writeback", - "not be forced into a tiny single-file step", + "Choose scope-bounded work toward a verifiable result", + "a focused correction can also be sufficient", + "obey budgets, explicit stops, settlement and replan requirements", "Stay inside goal_boundary when present", "Public-safe repo publication is not an operator gate by itself", "commit, push, and PR creation may proceed autonomously after validation", "clean public/private boundary scan", "private or company-internal material, credentials, destructive git operations, production actions", - "Run the smallest useful validation", + "Run validation proportionate to the change and risk", "Write back changed files, validation, critic, and next action", "Plan/top todo/route changes need todo/Next Action writeback", "If a user/owner todo appears", @@ -927,8 +929,8 @@ def main() -> int: "2 consecutive eligible heartbeats are no-progress loops", "self-cancel turn", "repair path is", - "Choose one bounded, verifiable progress segment from that audit", - "coherent batch across related implementation, test, doc, and state-writeback", + "授权/预算内推进可验证结果", + "a focused correction may suffice", "Stay inside `goal_boundary` when present", "Public-safe repo publication is not an operator gate by itself", "commit, push, and PR creation may proceed autonomously after validation", @@ -987,9 +989,9 @@ def main() -> int: "Run a short steering audit before choosing work", "Include a product bottleneck lens", "Run the no-progress self-repair check before choosing delivery work", - "Choose one bounded, verifiable progress segment from that audit", + "Choose scope-bounded work toward a verifiable result", "Public-safe repo publication is not an operator gate by itself", - "Run the smallest useful validation", + "Run validation proportionate to the change and risk", "loopx refresh-state --goal-id ", 'loopx --format json --registry "$HOME/.codex/loopx/registry.global.json" quota spend-slot --goal-id --todo-id --slots 1 --source heartbeat --execute', "If the dashboard or controller needs a state-only update after spend", diff --git a/examples/control_plane/quota-heartbeat-recommendation-state-machine-smoke.py b/examples/control_plane/quota-heartbeat-recommendation-state-machine-smoke.py index ddd7db7f89..a81a07b931 100644 --- a/examples/control_plane/quota-heartbeat-recommendation-state-machine-smoke.py +++ b/examples/control_plane/quota-heartbeat-recommendation-state-machine-smoke.py @@ -258,7 +258,7 @@ def assert_default_bounded_delivery_mode() -> None: lane=work_lane(open_count=1, advancement_count=1, monitor_count=0), ) assert normal["recommended_mode"] == "steering_audit_then_one_step", normal - assert "bounded progress segment" in normal["spend_policy"], normal + assert "scope-bounded work" in normal["spend_policy"], normal def main() -> int: diff --git a/loopx/control_plane/goals/start_contract.py b/loopx/control_plane/goals/start_contract.py index 3bcb47c54b..e85e0f7d84 100644 --- a/loopx/control_plane/goals/start_contract.py +++ b/loopx/control_plane/goals/start_contract.py @@ -172,7 +172,7 @@ def build_goal_start_prompt( agent_clause = f" Use agent id `{agent_id}` for quota/claim commands." if agent_id else "" todo_rule = ( "plan the broader direction as evidence-linked notes, but write exactly one " - "current small, verifiable Agent advancement_task Todo; do not write a runnable " + "current independently verifiable Agent advancement_task Todo; do not write a runnable " "successor ahead. Use `[P0]`/`[P1]`/`[P2]`, no `--priority`; User Todo only " "for owner/private gates" if fine_grained @@ -182,11 +182,11 @@ def build_goal_start_prompt( "Todo before work" ) fine_rule = ( - "\n8. Fine-grained mode: the current Todo must be one small verifiable checkpoint. " - "If it is too broad, split it before work. A coherent decision slice may complete " + "\n8. Fine-grained mode: the current Todo must be an independently verifiable checkpoint. " + "Split independent decisions before work. Scope-bounded work may complete " "one or more causally related Agent advancement Todos in the same turn: after each " "completion inspect its fresh evidence before creating or claiming the next Todo, " - "and settle only once after the slice. Use the existing replan obligation/ACK path " + "and settle only once after the work. Use the existing replan obligation/ACK path " "when evidence changes direction or the bounded-chain review becomes due; never " "prewrite a long runnable chain. Protocol/setup and capability re-entry steps stay " "inline in the guided transaction, are not Todos, and do not count as advancement " diff --git a/loopx/control_plane/heartbeat/builder.py b/loopx/control_plane/heartbeat/builder.py index 53ef60168a..a05ad7fe45 100644 --- a/loopx/control_plane/heartbeat/builder.py +++ b/loopx/control_plane/heartbeat/builder.py @@ -71,12 +71,12 @@ ) FINE_GRAINED_TURN_RULE = ( - "Fine-grained planning contract: each Todo must be one small verifiable checkpoint; " - "if broader, split before delivery. The turn budget is one coherent decision slice " + "Fine-grained planning contract: each Todo must be an independently verifiable checkpoint; " + "split independent decisions before delivery. Work follows one coherent direction " "and may complete one or more causally related Agent advancement Todos. After each " "completion inspect fresh evidence before creating or claiming a successor; continue " "only while the direction remains unchanged. Validate and durably complete each Todo, " - "then perform accountable refresh and spend to settle the turn once after the slice. " + "then perform accountable refresh and spend to settle the turn once after the work. " "A direction change or bounded-chain review must use the existing replan obligation/" "ACK path before further delivery. Protocol/setup and capability re-entry steps are " "inline non-advancement work: never create Todos or settle a turn for them alone." diff --git a/loopx/control_plane/heartbeat/rules.py b/loopx/control_plane/heartbeat/rules.py index 0ad712057f..f919ad8c46 100644 --- a/loopx/control_plane/heartbeat/rules.py +++ b/loopx/control_plane/heartbeat/rules.py @@ -3,6 +3,10 @@ DEFAULT_MATERIAL_QUEUE_RULE = "Do not consume the learning material queue unless the user explicitly asks." DEFAULT_PERMISSION_RULE = "Do not ask for permissions when the current Codex session is already trusted." +SCOPE_BOUNDED_WORK_RULE = ( + "授权/预算内推进可验证结果;按任务/证据/风险定规模,不按操作/文件数/心跳间隔。" + "操作/写回不自动结束;遵守停止/重规划。" +) USER_TODO_FINAL_MESSAGE_RULE = ( "`interaction_contract.user_channel.notify` controls output: `NOTIFY` -> concrete " "action; otherwise quiet. `should_run`/due monitor and other-agent scoped todos " @@ -12,11 +16,10 @@ ) HEARTBEAT_NOTIFICATION_RULE_SHORT = ( "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; " - "DONT_NOTIFY=安静输出。执行义务看 `heartbeat_recommendation.agent_must_attempt`/" - "`execution_obligation.must_attempt_work`:true 时必须执行 bounded slice 并写回," - "quiet no-op 仅当 false。" + "DONT_NOTIFY=安静输出。见 `heartbeat_recommendation.agent_must_attempt`/" + "`execution_obligation.must_attempt_work`:true须推进并写回,false才可no-op。" "Due/peer gate != prompt; missing NOTIFY action->" - "具体user todo未投影,需修复LoopX状态投影." + "具体user todo未投影." ) HEARTBEAT_NOTIFICATION_RULE_THIN = ( "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; " diff --git a/loopx/control_plane/heartbeat/task_body.py b/loopx/control_plane/heartbeat/task_body.py index f1c38f6d8f..2d2a5ae7d0 100644 --- a/loopx/control_plane/heartbeat/task_body.py +++ b/loopx/control_plane/heartbeat/task_body.py @@ -19,6 +19,7 @@ SCHEDULER_HINT_APPLICATION_RULE, SCHEDULER_HINT_COMPACT_RULE, SCHEDULER_HINT_THIN_RULE, + SCOPE_BOUNDED_WORK_RULE, USER_TODO_FINAL_MESSAGE_RULE, ) @@ -168,7 +169,7 @@ def render_heartbeat_task_body( the concrete blocker. Do not fall through to ordinary delivery, surface propagation, or synthetic-only chains. Read `execution_obligation`: `notify` is not an execution gate; - `must_attempt_work=true` means one bounded segment even with + `must_attempt_work=true` requires scope-bounded work even with `notify=DONT_NOTIFY`; quiet no-op needs `must_attempt_work=false` and `user_channel.notify=DONT_NOTIFY`. Use `scheduler_hint` for wakeup and unchanged-loop limits. For Codex App: @@ -210,11 +211,9 @@ def render_heartbeat_task_body( heartbeats are no-progress loops, run one bounded self-repair/replan segment before another quiet no-op. Delete/pause only when that repair path is stuck for 2 more eligible turns; no spend for the self-cancel turn. -4. Choose one bounded, verifiable progress segment from that audit. It may be a - coherent batch across related implementation, test, doc, and state-writeback - files when the write scope is clear and validation is explicit; it should not - be forced into a tiny single-file step. -5. Do that segment only. Stay inside `goal_boundary` when present and keep +4. {SCOPE_BOUNDED_WORK_RULE} + Related work can form a coherent effort; a focused correction may suffice. +5. Execute that scoped work. Stay inside `goal_boundary` when present and keep public/private boundaries intact. Public-safe repo publication is not an operator gate by itself: for routine public project work, commit, push, and PR creation may proceed autonomously after validation and a clean @@ -222,7 +221,7 @@ def render_heartbeat_task_body( for private or company-internal material, credentials, destructive git operations, production actions, or repository rules that explicitly require review. -6. Run the smallest useful validation. +6. Run validation proportionate to the change and risk. 7. Write back changed files, validation, critic, and next action to the active state. If a user/owner todo appears, do not hide it in prose: use `{cli_bin} todo add --goal-id {goal_id} --role user --task-class user_gate --blocks-agent ` @@ -307,6 +306,7 @@ def render_brief_heartbeat_task_body( Fail:quiet. {HEARTBEAT_NOTIFICATION_RULE_THIN} +{SCOPE_BOUNDED_WORK_RULE} {HEARTBEAT_VISION_WRITEBACK_RULE_SHORT} If `should_run=false`: follow user channel. `monitor_quiet_skip`: receipt/stall @@ -320,7 +320,7 @@ def render_brief_heartbeat_task_body( `execution_obligation`, `effective_action`, `recovery_delivery_allowed`, `heartbeat_recommendation`, `safe_bypass_kind=outcome_floor_recovery`, `goal_boundary`, `delivery_batch_scale`, `delivery_outcome`, outcome streaks, -`handoff_delivery_contract`; do 1 bounded segment/batch when +`handoff_delivery_contract`; advance scope-bounded work when `execution_obligation.must_attempt_work=true`; if recovery, run ranker/cross-domain evidence recovery or blocker writeback; validate/writeback/todos; {HOST_LOOP_TODO_CLOSEOUT_COMPACT_RULE} Progress(actual,no upgrade): @@ -360,9 +360,8 @@ def render_compact_heartbeat_task_body( ) return f"""Advance `{goal_id}` using `{active_state}`. -This compact LoopX heartbeat body; policy: -registry/state/adapter/`goal_boundary`. -Expanded lifecycle contract: `{expanded_prompt_command}`. +Compact policy: registry/state/adapter/`goal_boundary`. +Detail: `{expanded_prompt_command}`. {scope_block} Preflight/guard; `LOOPX_TURN=`; reuse: @@ -402,10 +401,8 @@ def render_compact_heartbeat_task_body( `safe_bypass_kind=outcome_floor_recovery`, run only ranker/cross-domain evidence artifact or blocker recovery; no ordinary delivery or surface/synthetic-only work. -4. Follow `execution_obligation`: `notify` is not an execution gate. - `must_attempt_work=true` means one bounded segment even with - `notify=DONT_NOTIFY`; quiet no-op needs `must_attempt_work=false` and - `user_channel.notify=DONT_NOTIFY`. +4. `execution_obligation`: `must_attempt_work=true` requires work even with + `notify=DONT_NOTIFY`; quiet no-op needs false and `user_channel.notify=DONT_NOTIFY`. Then follow `heartbeat_recommendation`: `run_first_read_only_map`: exact real-map, validate/save/refresh/spend; notify only under `NOTIFY`; @@ -423,7 +420,7 @@ def render_compact_heartbeat_task_body( `execution_obligation.must_attempt_work=true`; after 2 eligible stall heartbeats with only status/brief checks, replan before quiet no-op. Pause/delete only if repair stays stuck 2 more turns. -7. Choose one bounded segment; coherent batch is OK with clear validation. +7. {SCOPE_BOUNDED_WORK_RULE} Public-safe commit/push/PR may proceed after validation/clean scan. Stop for private/company material, credentials, destructive git, production, or review rules. 8. Validate; write files/validation/critic/next action to active state; @@ -575,9 +572,9 @@ def _render_goal_task_body( `should_run=true`: take highest-priority unblocked in-scope todo by default; choose any other eligible Todo with a reason. Honor claims/leases and blocker-push/recovery obligations. Before dependencies, persist changed scope/acceptance/non-goal evidence and next todo. -A bounded segment is progress within this Goal: a segment is progress, not a new Goal -boundary. Reuse this Goal until terminal; do not create a successor host Goal merely to -continue; do not create a successor merely to continue. Validate; write public-safe evidence. +{SCOPE_BOUNDED_WORK_RULE} +Progress is not a new Goal boundary. Reuse this Goal until terminal; +do not create a successor merely to continue. Validate; write public-safe evidence. {HOST_LOOP_TODO_CLOSEOUT_RULE} For classification/scale/outcome, never default or upgrade them to @@ -688,6 +685,7 @@ def render_thin_heartbeat_task_body( `LOOPX_TURN=`; reuse. {pr_review_pre_quota_instruction}{quota_guard_instruction}. {HEARTBEAT_NOTIFICATION_RULE_SHORT} +{SCOPE_BOUNDED_WORK_RULE} {RUNTIME_CAPABILITY_PROJECTION_THIN_RULE} {SCHEDULER_HINT_THIN_RULE} {HEARTBEAT_VISION_WRITEBACK_RULE_SHORT} diff --git a/loopx/control_plane/quota/heartbeat_recommendation.py b/loopx/control_plane/quota/heartbeat_recommendation.py index 74b7033317..2bc53e81c4 100644 --- a/loopx/control_plane/quota/heartbeat_recommendation.py +++ b/loopx/control_plane/quota/heartbeat_recommendation.py @@ -596,13 +596,13 @@ def _post_handoff_observation_rule( "recommended_mode": "post_handoff_observe_then_backlog_step", "spend_policy": ( "observe registry/status/run history/repo state first; if unchanged, " - "advance exactly one bounded agent-todo backlog segment and append " + "advance scope-bounded agent-todo work and append " "quota spend only after validation and durable writeback" ), "reason": ( "latest post-handoff implementation reached the primary outcome, " "but an open agent todo remains; observe for new blockers first, " - "then advance one bounded backlog step instead of quiet idling" + "then advance scope-bounded backlog work instead of quiet idling" ), }, ) @@ -616,8 +616,8 @@ def _default_rule( { "recommended_mode": "steering_audit_then_one_step", "spend_policy": ( - "append exactly one heartbeat spend only after a bounded progress " - "segment is validated and written back" + "append exactly one heartbeat spend only after scope-bounded work " + "is validated and written back; one_step names the mode, not an operation limit" ), "reason": ( "eligible Codex-ready goal requires the standard steering audit " diff --git a/loopx/control_plane/work_items/interaction_contract.py b/loopx/control_plane/work_items/interaction_contract.py index 4b741eee0d..a661a1d337 100644 --- a/loopx/control_plane/work_items/interaction_contract.py +++ b/loopx/control_plane/work_items/interaction_contract.py @@ -337,7 +337,7 @@ def protocol_action_packet_fields(payload: dict[str, Any]) -> dict[str, Any]: elif bounded_delivery_with_user_notice: primary_actor = "agent_with_user_gate" agent_action_required = True - agent_action = _protocol_first_candidate_action(payload) or "advance one bounded segment" + agent_action = _protocol_first_candidate_action(payload) or "advance scope-bounded work with validation" elif requires_user_action: primary_actor = "user" agent_action_required = False @@ -374,7 +374,7 @@ def protocol_action_packet_fields(payload: dict[str, Any]) -> dict[str, Any]: "the concrete blocker" ) else: - agent_action = _protocol_first_candidate_action(payload) or "advance one bounded segment" + agent_action = _protocol_first_candidate_action(payload) or "advance scope-bounded work with validation" else: primary_actor = "agent" agent_action_required = False diff --git a/loopx/control_plane/work_items/primary_action.py b/loopx/control_plane/work_items/primary_action.py index c6d46a7722..d49a54ea74 100644 --- a/loopx/control_plane/work_items/primary_action.py +++ b/loopx/control_plane/work_items/primary_action.py @@ -273,7 +273,7 @@ def resolve_canonical_primary_action(payload: dict[str, Any], *, mode: str) -> s "surface the scoped user gate, then advance one non-gated fallback" ) if mode == "bounded_delivery_with_user_notice": - return protocol_first_candidate_action(payload) or "advance one bounded validated segment" + return protocol_first_candidate_action(payload) or "advance scope-bounded work with validation" if mode == "task_orchestration": contract = ( payload.get("task_orchestration_contract") @@ -321,7 +321,7 @@ def resolve_canonical_primary_action(payload: dict[str, Any], *, mode: str) -> s if mode == "boundary_projection_repair": return "repair goal_boundary.write_scope projection before attempting the selected write" if mode == "bounded_delivery": - return protocol_first_candidate_action(payload) or "advance one bounded validated segment" + return protocol_first_candidate_action(payload) or "advance scope-bounded work with validation" if mode == "mapped_noop_if_unchanged": return "confirm no new instruction/evidence/todo/stale source/safe handoff, then quiet no-op" if execution_obligation.get("contract_obligation"): diff --git a/skills/loopx-project/SKILL.md b/skills/loopx-project/SKILL.md index ac12325880..85bf802a5e 100644 --- a/skills/loopx-project/SKILL.md +++ b/skills/loopx-project/SKILL.md @@ -460,8 +460,13 @@ any losing high-value candidate that should not be forgotten. Include a product bottleneck lens: ask whether the core goal is currently bottlenecked by user experience, agent capability, evidence quality, adapter readiness, or priority-rule gaps, and promote one concrete bottleneck candidate when it should -outrank the nearest local TODO. Then choose exactly one bounded, verifiable step -from that audit. +outrank the nearest local TODO. Choose scope-bounded work toward a verifiable +result. Size the work by the task, evidence and risk, not tool calls, file count +or heartbeat interval. Related implementation, research, validation and writeback +may form one coherent effort; a focused correction can also be sufficient. +One operation or writeback alone is not a reason to stop. Budget, scope, explicit +stop conditions, settlement and replan requirements still apply; this guidance +does not authorize a new Todo, lease, direction or external action. When you tell the user a connected LoopX plan, top-todo list, priority stack, or route change, treat that as a writeback trigger, not chat memory. If diff --git a/skills/loopx-self-repair/SKILL.md b/skills/loopx-self-repair/SKILL.md index eb24a6e3f8..b9a7cba2ec 100644 --- a/skills/loopx-self-repair/SKILL.md +++ b/skills/loopx-self-repair/SKILL.md @@ -39,7 +39,7 @@ not only an apology or a one-off explanation. - docs/process hygiene gap. 5. **Repair at the lowest durable layer.** - If it is a one-off agent mistake, write back the correct state/todo and - continue with a larger bounded batch. + size the next scoped effort to its verifiable result, evidence and risk. - If the machine projection misled the agent, fix CLI/status/quota projection and add a focused smoke. - If the user correction changes the goal acceptance, says the agent missed @@ -138,9 +138,10 @@ replan, or terminal closeout must return to the strict semantic checkpoint. - Do not let fallback work hide the primary blocker. When a higher-priority path is gated but safe fallback is valid, report both the concrete gate and the fallback progress. -- Do not let tiny safe steps become the default. If several recent turns are - short or surface-only, run a steering audit and increase the next bounded - batch size unless a real gate blocks it. +- Do not equate bounded work with a small operation. If turns repeatedly stop + after setup or surface-only edits, check whether a verifiable result could + have been reached within scope and budget. Repair the premature stop, not by + imposing a minimum number of calls/files or ignoring explicit stop conditions. ## Reference Routes diff --git a/tests/control_plane/test_fine_grained_turn_mode.py b/tests/control_plane/test_fine_grained_turn_mode.py index 63f26968d4..c3e7ce42dc 100644 --- a/tests/control_plane/test_fine_grained_turn_mode.py +++ b/tests/control_plane/test_fine_grained_turn_mode.py @@ -272,6 +272,9 @@ def test_fine_heartbeat_rule_is_opt_in_only() -> None: assert "one or more causally related" in fine["task_body"] assert "settle the turn once" in fine["task_body"] assert "Protocol/setup and capability re-entry" in fine["task_body"] + assert "independently verifiable checkpoint" in fine["task_body"] + assert "small verifiable checkpoint" not in fine["task_body"] + assert "After each completion inspect fresh evidence" in fine["task_body"] def test_heartbeat_cli_reads_sticky_fine_mode_from_registry(tmp_path: Path) -> None: diff --git a/tests/control_plane/test_heartbeat_prompt_support.py b/tests/control_plane/test_heartbeat_prompt_support.py index 7f1d1941f1..d75c2eedf4 100644 --- a/tests/control_plane/test_heartbeat_prompt_support.py +++ b/tests/control_plane/test_heartbeat_prompt_support.py @@ -16,6 +16,7 @@ uses_ark_managed_agent_goal_host, uses_native_goal_host_loop, ) +from loopx.control_plane.heartbeat.rules import SCOPE_BOUNDED_WORK_RULE from loopx.control_plane.heartbeat.visible_goal import ( build_visible_goal_initial_runtime_capability_projection, validate_visible_goal_policy_rule, @@ -128,6 +129,30 @@ def test_public_facade_still_builds_and_renders_prompts() -> None: assert render_heartbeat_prompt_markdown(payload) +@pytest.mark.parametrize("mode", ["full", "compact", "brief", "thin"]) +def test_sizing_guidance_survives_prompt_compaction(mode: str) -> None: + payload = build_heartbeat_prompt(goal_id="sizing-fixture", **{mode: True}) + body = payload["task_body"] + # Compaction must not discard how to size work while keeping only "one step". + assert body.count(SCOPE_BOUNDED_WORK_RULE) == 1 + assert "bounded slice" not in body + assert payload["interface_budget"]["within_budget"] is True + # This is guidance, not a new execution profile, scheduler or authority field. + assert "turn_mode" not in payload + assert "--fine-grained" not in payload["quota_guard_command"] + + +@pytest.mark.parametrize("profile", ["codex_cli", "ark_managed_agent_goal"]) +def test_goal_hosts_preserve_sizing_and_terminal_boundary(profile: str) -> None: + payload = build_heartbeat_prompt(goal_id="sizing-fixture", runtime_profile=profile) + body = payload["task_body"] + assert body.count(SCOPE_BOUNDED_WORK_RULE) == 1 + assert "`should_run=false`: no delivery/spend" in body + assert "terminal no-follow-up" in body + assert "Then spend exactly once" in body + assert payload["interface_budget"]["within_budget"] is True + + @pytest.mark.parametrize("mode", ["full", "compact", "brief", "thin"]) def test_exact_turn_identity_is_preserved_across_heartbeat_modes(mode: str) -> None: turn_id = "dsh-turn:2026-08-21T12:34:56Z" diff --git a/tests/control_plane/test_heartbeat_recommendation_rules.py b/tests/control_plane/test_heartbeat_recommendation_rules.py index f92ff2c7fa..7cf3774617 100644 --- a/tests/control_plane/test_heartbeat_recommendation_rules.py +++ b/tests/control_plane/test_heartbeat_recommendation_rules.py @@ -239,11 +239,12 @@ def test_post_handoff_primary_outcome_preserves_observation_contract() -> None: assert recommendation["latest_run"]["progress_scope"] == "primary_goal" -def test_default_mode_requires_bounded_steering_audit() -> None: +def test_default_mode_keeps_compatibility_without_an_operation_limit() -> None: recommendation = _recommend( {"status": "active-read-only"}, agent_open=1, ) assert recommendation["recommended_mode"] == "steering_audit_then_one_step" - assert "bounded progress segment" in recommendation["spend_policy"] + assert "scope-bounded work" in recommendation["spend_policy"] + assert "not an operation limit" in recommendation["spend_policy"] diff --git a/tests/test_host_loop_activation.py b/tests/test_host_loop_activation.py index ff21830f2c..b9da6728aa 100644 --- a/tests/test_host_loop_activation.py +++ b/tests/test_host_loop_activation.py @@ -401,7 +401,7 @@ def test_goal_hosts_share_narrow_runtime_skill_routing( "lifecycle/registry and `loopx-self-repair` for runtime/projection drift." in task_body ) - assert "A bounded segment is progress within this Goal" in task_body + assert "Progress is not a new Goal boundary" in task_body assert "do not create a successor merely to continue" in task_body From 846f66a264bf1026f6c5874c0a38aba816ee0e82 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 10 Sep 2026 19:44:10 +0800 Subject: [PATCH 2/3] fix(heartbeat): preserve projection repair in compact guidance Signed-off-by: huangruiteng --- docs/heartbeat-automation-prompt.md | 6 ++++++ .../control_plane/heartbeat-prompt-smoke.py | 18 ++++++++++-------- loopx/control_plane/heartbeat/rules.py | 7 ++++--- .../test_heartbeat_notification_rule.py | 17 +++++++++++++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/heartbeat-automation-prompt.md b/docs/heartbeat-automation-prompt.md index 8adc2f5f46..81062b0c22 100644 --- a/docs/heartbeat-automation-prompt.md +++ b/docs/heartbeat-automation-prompt.md @@ -39,6 +39,12 @@ two separate layers: runtime source of truth. This makes the Codex thread a replaceable worker and leaves durable task truth in LoopX. +Prompt compression must preserve both conditions and required responses. A +missing concrete user action under `NOTIFY` calls for state-projection repair, +not just a diagnostic label. Under `DONT_NOTIFY`, repair stays internal; it +does not grant notification authority. Keep these semantics in brief and thin +prompts as well as the expanded contract, within their existing size budgets. + Do not paste the full lifecycle protocol into the visible goal text, and do not use a short goal text such as "advance TODO" as the recurring automation body. The short text names the goal; the generated task body enforces quota, gates, diff --git a/examples/control_plane/heartbeat-prompt-smoke.py b/examples/control_plane/heartbeat-prompt-smoke.py index f881d08867..72bea59e88 100644 --- a/examples/control_plane/heartbeat-prompt-smoke.py +++ b/examples/control_plane/heartbeat-prompt-smoke.py @@ -64,9 +64,11 @@ def user_output_policy(task_body: str, *, mode: str) -> dict[str, str]: else: assert "`user_channel.notify` controls OUTPUT only" in body assert "NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出" in body - assert "Due/peer gate != prompt" in body - assert "missing NOTIFY action->" in body + assert "Due/peer非用户动作" in body + assert "NOTIFY缺动作→" in body assert "具体user todo未投影" in body + assert "需修复LoopX状态投影" in body + assert "静默时内部修复" in body if mode == "brief": assert "Return only under `user_channel.notify=NOTIFY`; else quiet." in body return { @@ -585,8 +587,8 @@ def main() -> int: "--goal-id loopx-meta --agent-id codex-product-capability --available-capability network " "--available-capability external_evidence_poll", "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出", - "Due/peer gate != prompt", - "missing NOTIFY action->", + "Due/peer非用户动作", + "NOTIFY缺动作→", "具体user todo未投影", "Observed capabilities -> `--available-capability`; never user gates", "host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)", @@ -642,9 +644,9 @@ def main() -> int: "Guard/retry; `LOOPX_TURN=`", 'loopx --format json --registry "$HOME/.codex/loopx/registry.global.json" quota should-run --goal-id public-heartbeat-goal', "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出", - "Due/peer gate != prompt", + "Due/peer非用户动作", "Done->successor first; final->refresh->spend->no-follow-up", - "missing NOTIFY action->", + "NOTIFY缺动作→", "具体user todo未投影", "follow user channel", "monitor_quiet_skip", @@ -685,8 +687,8 @@ def main() -> int: "use selection_command when required", "`quota should-run`", "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出", - "Due/peer gate != prompt", - "missing NOTIFY action->", + "Due/peer非用户动作", + "NOTIFY缺动作→", "具体user todo未投影", "host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)", "else RRULE/fallback_hint/ack/fail", diff --git a/loopx/control_plane/heartbeat/rules.py b/loopx/control_plane/heartbeat/rules.py index f919ad8c46..a7577c6c2a 100644 --- a/loopx/control_plane/heartbeat/rules.py +++ b/loopx/control_plane/heartbeat/rules.py @@ -18,13 +18,14 @@ "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; " "DONT_NOTIFY=安静输出。见 `heartbeat_recommendation.agent_must_attempt`/" "`execution_obligation.must_attempt_work`:true须推进并写回,false才可no-op。" - "Due/peer gate != prompt; missing NOTIFY action->" - "具体user todo未投影." + "Due/peer非用户动作;NOTIFY缺动作→" + "具体user todo未投影,需修复LoopX状态投影;静默时内部修复。" ) HEARTBEAT_NOTIFICATION_RULE_THIN = ( "`user_channel.notify` controls OUTPUT only: NOTIFY=向用户输出动作; " "DONT_NOTIFY=安静输出。执行义务看 `agent_must_attempt`/`must_attempt_work`。" - "Due/peer gate != prompt; missing NOTIFY action->具体user todo未投影." + "Due/peer非用户动作;NOTIFY缺动作→" + "具体user todo未投影,需修复LoopX状态投影;静默时内部修复。" ) HEARTBEAT_VISION_WRITEBACK_RULE_SHORT = ( "writeback: no-change=`surface_only`/no spend; " diff --git a/tests/control_plane/test_heartbeat_notification_rule.py b/tests/control_plane/test_heartbeat_notification_rule.py index 23b1895e00..348ea90539 100644 --- a/tests/control_plane/test_heartbeat_notification_rule.py +++ b/tests/control_plane/test_heartbeat_notification_rule.py @@ -3,12 +3,16 @@ The short notification rule used to read "DONT_NOTIFY=quiet", which agents can misread as "do nothing". The rule must qualify DONT_NOTIFY as an output-level signal only and keep `execution_obligation.must_attempt_work` as the authority -for whether a bounded slice must run. +for whether work must run. Compact wording must preserve the repair action, +not just diagnose a missing user action, without turning repair into a prompt. """ from __future__ import annotations -from loopx.control_plane.heartbeat.rules import HEARTBEAT_NOTIFICATION_RULE_SHORT +from loopx.control_plane.heartbeat.rules import ( + HEARTBEAT_NOTIFICATION_RULE_SHORT, + HEARTBEAT_NOTIFICATION_RULE_THIN, +) from loopx.control_plane.heartbeat.task_body import ( render_brief_heartbeat_task_body, render_thin_heartbeat_task_body, @@ -38,6 +42,13 @@ def test_short_rule_qualifies_dont_notify_as_output_only() -> None: assert "execution_obligation.must_attempt_work" in rule +def test_short_rules_keep_projection_repair_and_quiet_boundary() -> None: + for rule in (HEARTBEAT_NOTIFICATION_RULE_SHORT, HEARTBEAT_NOTIFICATION_RULE_THIN): + assert "NOTIFY缺动作→具体user todo未投影" in rule + assert "需修复LoopX状态投影" in rule + assert "静默时内部修复" in rule + + def test_rendered_task_bodies_keep_execution_obligation_authority() -> None: kwargs = dict( goal_id="fixture-goal", @@ -62,6 +73,8 @@ def test_rendered_task_bodies_keep_execution_obligation_authority() -> None: assert "agent_must_attempt" in body assert "execution_obligation.must_attempt_work" in body assert "OUTPUT only" in body + assert "需修复LoopX状态投影" in body + assert "静默时内部修复" in body # A bare "DONT_NOTIFY=quiet" no-op mapping must never appear in the prompt. assert "DONT_NOTIFY=quiet." not in body From 03f6b4083202e6ff4541f2c1f7d1a2f9bf3a2403 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 10 Sep 2026 21:21:30 +0800 Subject: [PATCH 3/3] fix(heartbeat): qualify bilingual thin budgets and host assertions Signed-off-by: huangruiteng --- .../control_plane/quota/heartbeat_recommendation.py | 2 +- .../testing/cli_output_differential.py | 6 ++++++ tests/control_plane/test_cli_output_differential.py | 13 +++++++++++++ tests/test_ark_managed_agent_host.py | 4 ++-- tests/test_ark_managed_agent_issue_fix_matrix.py | 4 ++-- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/loopx/control_plane/quota/heartbeat_recommendation.py b/loopx/control_plane/quota/heartbeat_recommendation.py index 2bc53e81c4..219194375d 100644 --- a/loopx/control_plane/quota/heartbeat_recommendation.py +++ b/loopx/control_plane/quota/heartbeat_recommendation.py @@ -617,7 +617,7 @@ def _default_rule( "recommended_mode": "steering_audit_then_one_step", "spend_policy": ( "append exactly one heartbeat spend only after scope-bounded work " - "is validated and written back; one_step names the mode, not an operation limit" + "is validated and written back; one_step: mode, not an operation limit" ), "reason": ( "eligible Codex-ready goal requires the standard steering audit " diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index a4e77378c2..d0feb0f8aa 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -465,6 +465,12 @@ def _compare_row(base: dict[str, Any], candidate: dict[str, Any]) -> dict[str, A metric=metric, base=base_value, ) + # Thin installed prompts contain bilingual lifecycle instructions. A + # small character-level clarification can cost three bytes per CJK + # character. Keep character, line and absolute output ceilings intact; + # do not relax quota or other agent-facing surfaces with this allowance. + if row_id.startswith("surface/heartbeat_prompt_thin/") and metric == "utf8_bytes": + allowance = max(allowance, 192) if migration.portfolio_growth_migration: allowance = max( allowance, diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py index 65316c98fa..2dac0ac73a 100644 --- a/tests/control_plane/test_cli_output_differential.py +++ b/tests/control_plane/test_cli_output_differential.py @@ -58,6 +58,19 @@ def _receipt(*rows: dict[str, object]) -> dict[str, object]: } +def test_thin_bilingual_byte_allowance_does_not_relax_character_or_quota_limits(): + from loopx.control_plane.testing.cli_output_differential import _compare_row + base = _row(row_id="surface/heartbeat_prompt_thin/small/markdown", format="markdown", + chars=100, utf8_bytes=100, lines=1, compact_payload_chars=100) + candidate = {**base, "chars": 120, "utf8_bytes": 260} + assert not _compare_row(base, candidate)["failures"] + assert _compare_row(base, {**candidate, "chars": 133})["failures"] + assert _compare_row(base, {**candidate, "utf8_bytes": 293})["failures"] + base["row_id"] = "surface/quota_should_run/small/markdown" + candidate["row_id"] = base["row_id"] + assert _compare_row(base, candidate)["failures"] + + def test_sync_commit_uses_main_as_cli_output_base() -> None: ancestors = { ("origin/main", "HEAD"), diff --git a/tests/test_ark_managed_agent_host.py b/tests/test_ark_managed_agent_host.py index 23fd02a1d1..c9405ea659 100644 --- a/tests/test_ark_managed_agent_host.py +++ b/tests/test_ark_managed_agent_host.py @@ -72,11 +72,11 @@ def test_goal_prompt_is_one_transport_independent_activation() -> None: assert "Goal runtime owns continuation and inner iterations" in normalized assert "goal loop, not automation" in normalized assert "invoke LoopX Turn" in normalized - assert "a segment is progress, not a new Goal boundary" in normalized + assert "Progress is not a new Goal boundary" in normalized assert ( "Reuse this Goal until terminal" ) in normalized - assert "do not create a successor host Goal merely to continue" in normalized + assert "do not create a successor merely to continue" in normalized assert ( "Normal turns use CLI `interaction_contract`; use `loopx-project` for " "lifecycle/registry and `loopx-self-repair` for runtime/projection drift." diff --git a/tests/test_ark_managed_agent_issue_fix_matrix.py b/tests/test_ark_managed_agent_issue_fix_matrix.py index 68547ec3b9..45220e2e88 100644 --- a/tests/test_ark_managed_agent_issue_fix_matrix.py +++ b/tests/test_ark_managed_agent_issue_fix_matrix.py @@ -166,8 +166,8 @@ def test_one_shot_host_contract_keeps_goal_closure_with_the_host() -> None: assert len(prompt["task_body"]) <= 4_000 task_body = prompt["task_body"] normalized = " ".join(task_body.split()) - assert "a segment is progress, not a new Goal boundary" in normalized - assert "do not create a successor host Goal merely to continue" in normalized + assert "Progress is not a new Goal boundary" in normalized + assert "do not create a successor merely to continue" in normalized assert "refresh the accountable progress record before spending" in normalized assert "Then spend exactly once against that refresh" in normalized assert task_body.index("loopx refresh-state") < task_body.index("quota spend-slot")