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..6237ee1b9c 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 +# SQLite qualification lanes pin an intermediate runtime above the minimum. +SQLITE_NODE_VERSION = "22.14" def declared_major(reference: str) -> str: @@ -52,15 +55,16 @@ 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(): 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} ) @@ -72,9 +76,15 @@ def main() -> int: 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 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, 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 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",