From 19c75e0e792e00ec710e42da7b3388da2ebc50f0 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 12 Sep 2026 02:37:09 +0800 Subject: [PATCH 1/4] fix(cli): restore module size and manpage classification budgets Two required public smokes fail on current `main`. `cli-command-module-size-ownership-command-modularization-smoke.py` reports `project_lifecycle.py has 1007 lines, above budget 1000`. The file crossed the budget when #4169 added the mutually exclusive external-sink delivery flags. Extract the three typed inline input codecs into `project_lifecycle_inputs.py`, which returns the owner to 925 lines without changing any public invocation. `cli-help-manpage-smoke.py` reports `unclassified: ['agent-context']`. The command shipped in #4244 without a manpage class. Add it to the existing `MANPAGE_COMMAND_HELP_ONLY` set, which is where comparable read-only lifecycle helpers already live. Extraction keeps the existing ownership contract: the registration and dispatch markers asserted by `cli-project-lifecycle-command-modularization-smoke.py` stay in the module, and `PROJECT_LIFECYCLE_COMMANDS` still covers all four commands. Validation: - `python3 examples/cli-command-module-size-ownership-command-modularization-smoke.py` -> ok - `python3 examples/cli-help-manpage-smoke.py` -> ok - `python3 examples/cli-project-lifecycle-command-modularization-smoke.py` -> ok - `python3 regression/cli-command-module-contract.py` -> ok - `python -m pytest tests/cli_commands/ tests/control_plane/test_cli_output_budget.py` -> 94 passed Signed-off-by: song --- loopx/cli_commands/project_lifecycle.py | 104 ++---------------- .../cli_commands/project_lifecycle_inputs.py | 99 +++++++++++++++++ loopx/help_surface.py | 1 + 3 files changed, 111 insertions(+), 93 deletions(-) create mode 100644 loopx/cli_commands/project_lifecycle_inputs.py diff --git a/loopx/cli_commands/project_lifecycle.py b/loopx/cli_commands/project_lifecycle.py index 88bfa12870..c69de7c16a 100644 --- a/loopx/cli_commands/project_lifecycle.py +++ b/loopx/cli_commands/project_lifecycle.py @@ -65,6 +65,11 @@ PostWritebackProjectionBuilder, dispatch_committed_cli_post_writeback_hooks, ) +from .project_lifecycle_inputs import ( + inline_agent_vision_packet, + inline_progress_observation, + reject_non_standard_json_constant, +) from .project_lifecycle_sinks import ( apply_external_sink_postcondition, lark_explore_graph_syncer, @@ -84,93 +89,6 @@ "operator-gate", } -INLINE_VISION_FIELDS = { - "vision_summary": "vision_summary", - "vision_role_scope": "role_scope", - "vision_acceptance": "acceptance_summary", - "vision_advancement_policy": "advancement_policy", - "vision_replan_trigger": "replan_trigger_summary", - "vision_dreaming_policy": "dreaming_policy", - "vision_last_patch": "last_patch_summary", -} - - -def _inline_agent_vision_packet(args: argparse.Namespace) -> dict[str, object] | None: - patch = { - field: str(value).strip() - for attr, field in INLINE_VISION_FIELDS.items() - for value in [getattr(args, attr, None)] - if str(value or "").strip() - } - todo_delta = [ - str(item or "").strip() - for item in (getattr(args, "vision_todo_delta", None) or []) - if str(item or "").strip() - ] - state = str(getattr(args, "vision_state", None) or "").strip() - if not patch and not todo_delta and not state: - return None - if not str(getattr(args, "agent_id", None) or "").strip(): - raise ValueError("inline agent vision requires --agent-id") - if not patch: - raise ValueError("inline agent vision requires at least one --vision-* patch field") - packet: dict[str, object] = { - "schema_version": "goal_vision_replan_contract_v0", - "vision_patch": patch, - "todo_delta": todo_delta, - } - if state: - packet["state"] = state - return packet - - -def _reject_non_standard_json_constant(name: str) -> object: - # json.loads would otherwise accept NaN/Infinity/-Infinity, which json.dump - # then re-emits as non-standard JSON that breaks strict ledger consumers. - raise ValueError( - f"--usage-json must be strict JSON; non-standard constant {name} is not allowed" - ) - - -def _inline_progress_observation( - args: argparse.Namespace, -) -> dict[str, object] | None: - fields = { - "surface_id": getattr(args, "progress_surface_id", None), - "hypothesis_id": getattr(args, "progress_hypothesis_id", None), - "probe_kind": getattr(args, "progress_probe_kind", None), - "result_class": getattr(args, "progress_result_class", None), - "blocker_id": getattr(args, "progress_blocker_id", None), - "coverage_scope_id": getattr(args, "progress_coverage_scope_id", None), - "coverage_complete": getattr(args, "progress_coverage_complete", None), - } - evidence_ids = list(getattr(args, "progress_evidence_ids", None) or []) - if not any(value is not None for value in fields.values()) and not evidence_ids: - return None - if not fields["result_class"]: - raise ValueError("typed progress observation requires --progress-result-class") - if fields["result_class"] in { - ProgressResultClass.EXPLORATION_EXHAUSTED.value, - ProgressResultClass.NO_FOLLOWUP.value, - } and not fields["coverage_scope_id"]: - raise ValueError( - f"--progress-result-class {fields['result_class']} requires " - "--progress-coverage-scope-id" - ) - if ( - fields["result_class"] == ProgressResultClass.EXPLORATION_EXHAUSTED.value - and fields["coverage_complete"] is not True - ): - raise ValueError( - "--progress-result-class exploration_exhausted requires " - "--progress-coverage-complete" - ) - return { - "schema_version": "typed_progress_observation_v0", - **{key: value for key, value in fields.items() if value is not None}, - "evidence_ids": evidence_ids, - } - def register_project_lifecycle_commands( subparsers: argparse._SubParsersAction, @@ -601,8 +519,8 @@ def handle_project_lifecycle_command( progress_observation: dict[str, object] | None = None merge_agent_vision_patch = False try: - inline_agent_vision_packet = _inline_agent_vision_packet(args) - if args.agent_vision_json and inline_agent_vision_packet: + inline_vision_packet = inline_agent_vision_packet(args) + if args.agent_vision_json and inline_vision_packet: raise ValueError( "--agent-vision-json cannot be combined with inline --vision-* fields" ) @@ -610,15 +528,15 @@ def handle_project_lifecycle_command( agent_vision_packet = json.loads( Path(args.agent_vision_json).expanduser().read_text(encoding="utf-8") ) - elif inline_agent_vision_packet: - agent_vision_packet = inline_agent_vision_packet + elif inline_vision_packet: + agent_vision_packet = inline_vision_packet merge_agent_vision_patch = True - progress_observation = _inline_progress_observation(args) + progress_observation = inline_progress_observation(args) usage_measurement: dict[str, object] | None = None if getattr(args, "usage_json", None): loaded_usage = json.loads( args.usage_json, - parse_constant=_reject_non_standard_json_constant, + parse_constant=reject_non_standard_json_constant, ) if not isinstance(loaded_usage, dict): raise ValueError("--usage-json must be a JSON object") diff --git a/loopx/cli_commands/project_lifecycle_inputs.py b/loopx/cli_commands/project_lifecycle_inputs.py new file mode 100644 index 0000000000..fd13b1413f --- /dev/null +++ b/loopx/cli_commands/project_lifecycle_inputs.py @@ -0,0 +1,99 @@ +"""Typed inline input codecs for project lifecycle CLI arguments. + +These helpers translate repeated CLI flags into the typed packets the refresh +path already accepts. They stay separate from the command owner so the +registration and dispatch module keeps one cohesive responsibility. +""" + +from __future__ import annotations + +import argparse + +from ..control_plane.work_items.progress_observation import ProgressResultClass + +INLINE_VISION_FIELDS = { + "vision_summary": "vision_summary", + "vision_role_scope": "role_scope", + "vision_acceptance": "acceptance_summary", + "vision_advancement_policy": "advancement_policy", + "vision_replan_trigger": "replan_trigger_summary", + "vision_dreaming_policy": "dreaming_policy", + "vision_last_patch": "last_patch_summary", +} + + +def inline_agent_vision_packet(args: argparse.Namespace) -> dict[str, object] | None: + patch = { + field: str(value).strip() + for attr, field in INLINE_VISION_FIELDS.items() + for value in [getattr(args, attr, None)] + if str(value or "").strip() + } + todo_delta = [ + str(item or "").strip() + for item in (getattr(args, "vision_todo_delta", None) or []) + if str(item or "").strip() + ] + state = str(getattr(args, "vision_state", None) or "").strip() + if not patch and not todo_delta and not state: + return None + if not str(getattr(args, "agent_id", None) or "").strip(): + raise ValueError("inline agent vision requires --agent-id") + if not patch: + raise ValueError("inline agent vision requires at least one --vision-* patch field") + packet: dict[str, object] = { + "schema_version": "goal_vision_replan_contract_v0", + "vision_patch": patch, + "todo_delta": todo_delta, + } + if state: + packet["state"] = state + return packet + + +def reject_non_standard_json_constant(name: str) -> object: + # json.loads would otherwise accept NaN/Infinity/-Infinity, which json.dump + # then re-emits as non-standard JSON that breaks strict ledger consumers. + raise ValueError( + f"--usage-json must be strict JSON; non-standard constant {name} is not allowed" + ) + + +def inline_progress_observation( + args: argparse.Namespace, +) -> dict[str, object] | None: + fields = { + "surface_id": getattr(args, "progress_surface_id", None), + "hypothesis_id": getattr(args, "progress_hypothesis_id", None), + "probe_kind": getattr(args, "progress_probe_kind", None), + "result_class": getattr(args, "progress_result_class", None), + "blocker_id": getattr(args, "progress_blocker_id", None), + "coverage_scope_id": getattr(args, "progress_coverage_scope_id", None), + "coverage_complete": getattr(args, "progress_coverage_complete", None), + } + evidence_ids = list(getattr(args, "progress_evidence_ids", None) or []) + if not any(value is not None for value in fields.values()) and not evidence_ids: + return None + if not fields["result_class"]: + raise ValueError("typed progress observation requires --progress-result-class") + if fields["result_class"] in { + ProgressResultClass.EXPLORATION_EXHAUSTED.value, + ProgressResultClass.NO_FOLLOWUP.value, + } and not fields["coverage_scope_id"]: + raise ValueError( + f"--progress-result-class {fields['result_class']} requires " + "--progress-coverage-scope-id" + ) + if ( + fields["result_class"] == ProgressResultClass.EXPLORATION_EXHAUSTED.value + and fields["coverage_complete"] is not True + ): + raise ValueError( + "--progress-result-class exploration_exhausted requires " + "--progress-coverage-complete" + ) + return { + "schema_version": "typed_progress_observation_v0", + **{key: value for key, value in fields.items() if value is not None}, + "evidence_ids": evidence_ids, + } diff --git a/loopx/help_surface.py b/loopx/help_surface.py index 918f042d21..bccd4547d4 100644 --- a/loopx/help_surface.py +++ b/loopx/help_surface.py @@ -302,6 +302,7 @@ # command an intentional manual-visibility decision instead of a silent omission. MANPAGE_COMMAND_HELP_ONLY = frozenset( { + "agent-context", "archive-runtime", "automation-prompts", "authority-shadow", From 48b029bafdad844e532d7ed33db5094e2cc260d0 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 12 Sep 2026 02:37:20 +0800 Subject: [PATCH 2/4] test(smokes): realign stale contracts with shipped behavior Four required public smokes assert contracts that have since moved. Each is reproducible on a clean `main@fa57253a7`, so this aligns the checks with the shipped behavior rather than changing any product path. - `blocker-push-runtime-smoke.py` asserted the retired per-shell phrasing `` `LOOPX_TURN=`; reuse. ``. #4201 moved the bootstrap rule into the shared `HEARTBEAT_TURN_BOOTSTRAP_RULE`, whose current sentence ends with `reuse the value on retries`. Assert that sentence. - `install-local-smoke.py` required the accountable refresh and spend commands inside the `--brief` prompt, but brief mode renders exactly one bounded guard block by design; those commands belong to the full and compact modes. #4201 already realigned the adjacent thin-mode assertions and missed this one. Assert the brief contract, including that the pair stays out. - `github-actions-runtime-smoke.py` rejected the `22.14` SQLite runtime and the Node 26 forward job, and required the pre-#4241 `merge-gate` needs order. Record `SQLITE_NODE_VERSION` with its finalization rationale, extend the `python-tests.yml` expectation, and match the current needs list. - `repository-hygiene-smoke.py` fails because the `v1.0.3` tag exists without a timeline entry. Add the entry, following the existing format. Validation (each command exits 0): - `python3 examples/blocker-push-runtime-smoke.py` - `python3 examples/install-local-smoke.py` - `python3 examples/github-actions-runtime-smoke.py` - `python3 examples/repository-hygiene-smoke.py` - `python3 examples/release/release-readiness-doc-smoke.py` - `python -m pytest tests/control_plane/test_heartbeat_notification_rule.py tests/control_plane/test_heartbeat_prompt_support.py tests/control_plane/test_heartbeat_receipt.py tests/control_plane/test_heartbeat_recommendation_rules.py` -> 48 passed Signed-off-by: song --- docs/product/release-readiness.md | 13 +++++++++++++ examples/blocker-push-runtime-smoke.py | 4 +++- examples/github-actions-runtime-smoke.py | 8 ++++++-- examples/install-local-smoke.py | 15 ++++++++------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/product/release-readiness.md b/docs/product/release-readiness.md index 0aabaad849..4b303272f3 100644 --- a/docs/product/release-readiness.md +++ b/docs/product/release-readiness.md @@ -562,6 +562,19 @@ path, and canary route rather than as a user-facing release baseline. paths gain bounded reads and clearer recovery diagnostics. The published wheel, source distribution, macOS, Windows, checksum, update, and PyPI artifacts were verified against the exact release source before promotion. +- `v1.0.3` on 2026-09-11 11:53 +08:00: native monitor observation and consumer + closure release at the matching `v1.0.3` tag (`0496975e`). Native monitor + observations and their successors commit atomically through one typed + transaction, and completed history stays out of target-key selection (#4187); + quota unifies typed scope selection without erasing user gates, and the + scheduler hint accepts canonical Base64 transport; archived history preserves + decision and resume semantics (#4184); the manager keeps concrete Core + findings in progress reports behind scoped evidence reads (#4213, #4218); + Goal Channels extract runtime command ownership (#4154) and deliver manager + terminal failure receipts (#4217); and iteration-fresh host dispatch plus + typed upstream terminal errors land through #4126 and #4215. The published + wheel, source distribution, macOS, Windows, checksum, update, and PyPI + artifacts were verified against the exact release source before promotion. When a new public release is promoted, add it here only after the matching tag, release note, stable ref, update path, and focused release canary agree. diff --git a/examples/blocker-push-runtime-smoke.py b/examples/blocker-push-runtime-smoke.py index 6b26737bc8..74b7b86bd8 100644 --- a/examples/blocker-push-runtime-smoke.py +++ b/examples/blocker-push-runtime-smoke.py @@ -214,7 +214,9 @@ def main() -> int: assert "NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出" in compact_prompt, prompt assert "Due/peer非用户动作" in compact_prompt, prompt assert "NOTIFY缺动作→具体user todo未投影" in compact_prompt, prompt - assert "`LOOPX_TURN=`; reuse." in compact_prompt, prompt + # The bootstrap rule is shared from heartbeat.rules after #4201; assert the + # current compact sentence instead of the retired per-shell phrasing. + assert "reuse the value on retries" in compact_prompt, prompt assert "guard receipt; 2 stalls->replan" in compact_prompt, prompt assert "no-change=`surface_only`/no spend" in compact_prompt, prompt assert "unchanged->`--vision-unchanged-reason`" in compact_prompt, prompt diff --git a/examples/github-actions-runtime-smoke.py b/examples/github-actions-runtime-smoke.py index 477f124e1e..090abd6bf1 100644 --- a/examples/github-actions-runtime-smoke.py +++ b/examples/github-actions-runtime-smoke.py @@ -21,6 +21,9 @@ PRIMARY_NODE_VERSION = "24" MINIMUM_NODE_VERSION = "22.6" FORWARD_NODE_VERSION = "26" +# SQLite conformance requires qualified statement finalization (22.14+), so the +# two SQLite integration jobs pin an intermediate runtime above the minimum. +SQLITE_NODE_VERSION = "22.14" def declared_major(reference: str) -> str: @@ -60,7 +63,7 @@ def main() -> int: if not versions: continue expected = ( - {PRIMARY_NODE_VERSION, MINIMUM_NODE_VERSION, FORWARD_NODE_VERSION} + {PRIMARY_NODE_VERSION, MINIMUM_NODE_VERSION, FORWARD_NODE_VERSION, SQLITE_NODE_VERSION} if name == "python-tests.yml" else {PRIMARY_NODE_VERSION} ) @@ -69,12 +72,13 @@ def main() -> int: python_versions = declared_versions["python-tests.yml"] assert python_versions.count(MINIMUM_NODE_VERSION) == 1, python_versions assert python_versions.count(FORWARD_NODE_VERSION) == 1, python_versions + assert python_versions.count(SQLITE_NODE_VERSION) == 2, python_versions assert PRIMARY_NODE_VERSION in python_versions, python_versions python_workflow = workflows["python-tests.yml"] assert "node-forward-compatibility:" in python_workflow assert "continue-on-error: true" in python_workflow - assert "needs: [changes, pytest, node-minimum-compatibility," in python_workflow + assert "needs: [changes, checks, pytest, node-minimum-compatibility," in python_workflow package = json.loads((ROOT / "package.json").read_text(encoding="utf-8")) assert package["engines"]["node"] == f">={MINIMUM_NODE_VERSION}" diff --git a/examples/install-local-smoke.py b/examples/install-local-smoke.py index 4701cfa4d9..7d710d61c7 100644 --- a/examples/install-local-smoke.py +++ b/examples/install-local-smoke.py @@ -822,13 +822,14 @@ def main() -> int: assert "loopx-canary --format json" in canary_payload["quota_guard_command"], canary_payload assert "loopx-canary heartbeat-prompt --compact" in canary_payload["task_body"], canary_payload canary_task_body = canary_payload["task_body"] - progress_command = canary_payload["progress_refresh_state_command"] - spend_command = canary_payload["quota_spend_command"] - assert progress_command in canary_task_body, canary_payload - assert spend_command in canary_task_body, canary_payload - assert canary_task_body.index(progress_command) < canary_task_body.index( - spend_command - ), canary_payload + # Brief mode renders one bounded guard block: it deliberately omits the + # accountable refresh/spend pair, which belongs to the full and compact + # modes. Assert the brief contract instead of the retired sequence. + assert canary_payload["quota_guard_command"] in canary_task_body, canary_payload + assert canary_payload["progress_refresh_state_command"] not in canary_task_body, canary_payload + assert canary_payload["quota_spend_command"] not in canary_task_body, canary_payload + assert "```bash\n" in canary_task_body and "LOOPX_TURN=" in canary_task_body, canary_payload + assert "not a command-prefix assignment" in canary_task_body, canary_payload fresh_install = run_install(env, "install-smoke-fresh") assert "loopx installed locally" in fresh_install.stdout, fresh_install.stdout From fe36c4ee08b2f72e4df9c462d21c59a38b6862cf Mon Sep 17 00:00:00 2001 From: song Date: Sat, 12 Sep 2026 09:04:08 +0800 Subject: [PATCH 3/4] test(ci): qualify runtime pins per workflow lane Signed-off-by: song --- examples/github-actions-runtime-smoke.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/github-actions-runtime-smoke.py b/examples/github-actions-runtime-smoke.py index 090abd6bf1..915f7d3dab 100644 --- a/examples/github-actions-runtime-smoke.py +++ b/examples/github-actions-runtime-smoke.py @@ -22,7 +22,7 @@ MINIMUM_NODE_VERSION = "22.6" FORWARD_NODE_VERSION = "26" # SQLite conformance requires qualified statement finalization (22.14+), so the -# two SQLite integration jobs pin an intermediate runtime above the minimum. +# SQLite qualification lanes pin an intermediate runtime above the minimum. SQLITE_NODE_VERSION = "22.14" @@ -72,10 +72,15 @@ def main() -> int: python_versions = declared_versions["python-tests.yml"] assert python_versions.count(MINIMUM_NODE_VERSION) == 1, python_versions assert python_versions.count(FORWARD_NODE_VERSION) == 1, python_versions - assert python_versions.count(SQLITE_NODE_VERSION) == 2, python_versions assert PRIMARY_NODE_VERSION in python_versions, python_versions python_workflow = workflows["python-tests.yml"] + jobs = dict(re.findall( + r"^ ([a-z][a-z0-9-]*):\n(.*?)(?=^ [a-z][a-z0-9-]*:\n|\Z)", + python_workflow, re.MULTILINE | re.DOTALL, + )) + for name in ("kernel-static-checks", "dashboard-acceptance", "windows-powershell"): + assert f'node-version: "{SQLITE_NODE_VERSION}"' in jobs[name], name assert "node-forward-compatibility:" in python_workflow assert "continue-on-error: true" in python_workflow assert "needs: [changes, checks, pytest, node-minimum-compatibility," in python_workflow From ad00e427495861c4cf98ead428bffbeeb90ed0fc Mon Sep 17 00:00:00 2001 From: song Date: Sat, 12 Sep 2026 09:07:42 +0800 Subject: [PATCH 4/4] test(ci): ignore comments when checking qualified runtime pins Signed-off-by: song --- examples/github-actions-runtime-smoke.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/github-actions-runtime-smoke.py b/examples/github-actions-runtime-smoke.py index 915f7d3dab..6237ee1b9c 100644 --- a/examples/github-actions-runtime-smoke.py +++ b/examples/github-actions-runtime-smoke.py @@ -55,8 +55,9 @@ def main() -> int: assert references, f"missing workflow reference for {action}" assert all(declared_major(reference) == major for reference in references), references + node_version_pattern = re.compile(r'^\s*node-version:\s*["\']([^"\']+)["\']\s*$', re.MULTILINE) declared_versions = { - name: re.findall(r'^\s*node-version:\s*["\']([^"\']+)["\']\s*$', text, re.MULTILINE) + name: node_version_pattern.findall(text) for name, text in workflows.items() } for name, versions in declared_versions.items(): @@ -80,7 +81,7 @@ def main() -> int: python_workflow, re.MULTILINE | re.DOTALL, )) for name in ("kernel-static-checks", "dashboard-acceptance", "windows-powershell"): - assert f'node-version: "{SQLITE_NODE_VERSION}"' in jobs[name], name + assert SQLITE_NODE_VERSION in node_version_pattern.findall(jobs[name]), name assert "node-forward-compatibility:" in python_workflow assert "continue-on-error: true" in python_workflow assert "needs: [changes, checks, pytest, node-minimum-compatibility," in python_workflow