Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/product/release-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion examples/blocker-push-runtime-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=<current_time_iso>`; 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
Expand Down
16 changes: 13 additions & 3 deletions examples/github-actions-runtime-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}
)
Expand All @@ -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}"
Expand Down
15 changes: 8 additions & 7 deletions examples/install-local-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=<current_time_iso>" 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
Expand Down
104 changes: 11 additions & 93 deletions loopx/cli_commands/project_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -601,24 +519,24 @@ 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"
)
if args.agent_vision_json:
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")
Expand Down
99 changes: 99 additions & 0 deletions loopx/cli_commands/project_lifecycle_inputs.py
Original file line number Diff line number Diff line change
@@ -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,
}
1 change: 1 addition & 0 deletions loopx/help_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down