From 9e74503604a15a7a5c2711649829d2a9a86c06cb Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Fri, 11 Sep 2026 09:50:35 +0800 Subject: [PATCH 1/5] chore(release): prepare LoopX 1.0.3 Signed-off-by: huangruiteng --- docs/book/chapters/00-reading-guide.md | 2 +- docs/book/en/chapters/00-reading-guide.md | 4 ++-- docs/book/en/index.md | 2 +- docs/book/index.md | 2 +- docs/product/release-readiness.md | 8 ++++++++ loopx/__init__.py | 2 +- man/loopx.1 | 2 +- pyproject.toml | 2 +- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/book/chapters/00-reading-guide.md b/docs/book/chapters/00-reading-guide.md index e0c8869d66..cff48d50fa 100644 --- a/docs/book/chapters/00-reading-guide.md +++ b/docs/book/chapters/00-reading-guide.md @@ -104,7 +104,7 @@ lifecycle 选择,不是所有贡献的默认终点。 ## 版本基线 -当前内容以 LoopX GitHub release `v1.0.2` 为发布锚点;本地命令示例已按 `loopx 1.0.2` 的 +当前内容以 LoopX GitHub release `v1.0.3` 为发布锚点;本地命令示例已按 `loopx 1.0.3` 的 公开 CLI 与协议表面复核。该版本要求 Python 3.11+ 与 Node.js 22.6+;后者运行由 LoopX 自动管理、 空闲后退出的 TypeScript Effect runtime,用户不需要手工维护 daemon。 diff --git a/docs/book/en/chapters/00-reading-guide.md b/docs/book/en/chapters/00-reading-guide.md index fe7dc7f39d..83883a308c 100644 --- a/docs/book/en/chapters/00-reading-guide.md +++ b/docs/book/en/chapters/00-reading-guide.md @@ -116,8 +116,8 @@ Do not bypass a newer permission or lifecycle check just to make an older exampl ## Version baseline -The current release anchor is LoopX GitHub release `v1.0.2`. Local command examples were checked against -the public `loopx 1.0.2` CLI and protocol surface. This release requires Python 3.11+ and Node.js 22.6+. +The current release anchor is LoopX GitHub release `v1.0.3`. Local command examples were checked against +the public `loopx 1.0.3` CLI and protocol surface. This release requires Python 3.11+ and Node.js 22.6+. LoopX starts and reuses its managed, idle-exiting TypeScript Effect runtime automatically; users do not operate that runtime as a manual daemon. diff --git a/docs/book/en/index.md b/docs/book/en/index.md index 8ee15b6ef1..339adb20ea 100644 --- a/docs/book/en/index.md +++ b/docs/book/en/index.md @@ -79,7 +79,7 @@ versioned or optional functionality, not the default shape of every contribution - Source format: Markdown - Site generator: MkDocs Material - Hosting: GitHub Pages -- LoopX release anchor: `v1.0.2` +- LoopX release anchor: `v1.0.3` - Runtime prerequisites: Python 3.11+ and Node.js 22.6+ The official public protocols remain authoritative for protocol facts. Commands that change across diff --git a/docs/book/index.md b/docs/book/index.md index 7e973b8a1e..25fddbbf8d 100644 --- a/docs/book/index.md +++ b/docs/book/index.md @@ -70,7 +70,7 @@ Extension 只是可独立版本化和交付的一种路径。 - 正文格式:Markdown; - 站点生成器:MkDocs Material; - 在线发布:GitHub Pages; -- LoopX 发布锚点:`v1.0.2`; +- LoopX 发布锚点:`v1.0.3`; - 运行时前提:Python 3.11+ 与 Node.js 22.6+。 协议解释以 LoopX 官方公开合同为事实源。易变化的命令仍以对应发布物、官方文档和当前 diff --git a/docs/product/release-readiness.md b/docs/product/release-readiness.md index cbaaee4ab9..1941344809 100644 --- a/docs/product/release-readiness.md +++ b/docs/product/release-readiness.md @@ -550,6 +550,14 @@ path, and canary route rather than as a user-facing release baseline. and frozen bundles install the same version-bound workflow skills as package distributions. The exact-tag Python, PyPI, macOS, Windows, signed-update, public-smoke, and live-model gates passed before `stable` fast-forwarded. +- `v1.0.2` on 2026-09-09 11:22 +08:00: single-owner Todo authority and + recovery release at the matching `v1.0.2` tag (`a5374d5b`). Promoted Todo + terminal transitions commit through one TypeScript-owned provider + transaction; missing generated Todo projections recover without making + Markdown authoritative; and long-history, Desktop, DSH, and managed-skill + 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. 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/loopx/__init__.py b/loopx/__init__.py index c3b8fbf9ec..9c22e7d62c 100644 --- a/loopx/__init__.py +++ b/loopx/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "1.0.2" +__version__ = "1.0.3" diff --git a/man/loopx.1 b/man/loopx.1 index fce53f944c..ba3638a58e 100644 --- a/man/loopx.1 +++ b/man/loopx.1 @@ -1,4 +1,4 @@ -.TH LOOPX 1 "" "LoopX 1.0.2" "User Commands" +.TH LOOPX 1 "" "LoopX 1.0.3" "User Commands" .SH NAME loopx \- control-plane helper for long-running agent work .SH SYNOPSIS diff --git a/pyproject.toml b/pyproject.toml index f0647f8351..a7a51d87d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "loopx" -version = "1.0.2" +version = "1.0.3" description = "A lightweight Loop Engineering control plane for long-running agent goals." readme = "README.md" requires-python = ">=3.11" From 1227d32dc93f4f875f3635a09467615b87a7515f Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 10 Sep 2026 07:18:03 +0800 Subject: [PATCH 2/5] fix(control-plane): restore public smoke contracts Signed-off-by: huangruiteng --- loopx/cli_commands/support_control.py | 53 ++----------------- .../support_control_agent_runtime.py | 33 ++++++++++++ loopx/control_plane/todos/authoring_scope.ts | 8 ++- loopx/control_plane/todos/field_update.ts | 26 ++++----- .../legacy_continuation_policy_migration.ts | 43 +++++++++++++++ loopx/control_plane/todos/resume_condition.ts | 5 ++ .../todo_authoring_scope.test.ts | 4 ++ .../todo_field_update.test.ts | 5 +- 8 files changed, 108 insertions(+), 69 deletions(-) create mode 100644 loopx/cli_commands/support_control_agent_runtime.py create mode 100644 loopx/control_plane/todos/legacy_continuation_policy_migration.ts diff --git a/loopx/cli_commands/support_control.py b/loopx/cli_commands/support_control.py index 9731a4360e..f332740b32 100644 --- a/loopx/cli_commands/support_control.py +++ b/loopx/cli_commands/support_control.py @@ -63,6 +63,7 @@ handle_backup_state_command, register_backup_state_command, ) +from .support_control_agent_runtime import register_agent_runtime_arguments from .support_control_chat_endpoint import ( handle_chat_endpoint_command, register_chat_endpoint_command, @@ -325,31 +326,7 @@ def register_support_control_commands( "--host", default=DEFAULT_CHAT_HOST, help="Loopback bind host." ) chat_parser.add_argument("--port", type=int, default=DEFAULT_CHAT_PORT) - chat_parser.add_argument( - "--codex-bin", - default="codex", - help="Codex CLI executable used for the read-only app-server session.", - ) - chat_parser.add_argument( - "--claude-bin", - default="claude", - help="Claude Code CLI executable used for read-only Agent sessions.", - ) - chat_parser.add_argument( - "--kiro-cli-bin", - default=KIRO_CLI_BIN, - help=( - "Kiro CLI executable used for read-only ACP Agent sessions " - "(` acp`)." - ), - ) - chat_parser.add_argument( - "--lark-cli-bin", - help=( - "Optional explicit lark-cli executable. When omitted, LoopX uses its bounded " - "runtime discovery order." - ), - ) + register_agent_runtime_arguments(chat_parser, kiro_cli_bin=KIRO_CLI_BIN) chat_parser.add_argument( "--startup-timeout-seconds", type=float, @@ -424,31 +401,7 @@ def register_support_control_commands( "--host", default=DEFAULT_CHAT_HOST, help="Loopback bind host." ) dashboard_parser.add_argument("--port", type=int, default=DEFAULT_CHAT_PORT) - dashboard_parser.add_argument( - "--codex-bin", - default="codex", - help="Codex CLI executable used for the read-only app-server session.", - ) - dashboard_parser.add_argument( - "--claude-bin", - default="claude", - help="Claude Code CLI executable used for read-only Agent sessions.", - ) - dashboard_parser.add_argument( - "--kiro-cli-bin", - default=KIRO_CLI_BIN, - help=( - "Kiro CLI executable used for read-only ACP Agent sessions " - "(` acp`)." - ), - ) - dashboard_parser.add_argument( - "--lark-cli-bin", - help=( - "Optional explicit lark-cli executable. When omitted, LoopX uses its bounded " - "runtime discovery order." - ), - ) + register_agent_runtime_arguments(dashboard_parser, kiro_cli_bin=KIRO_CLI_BIN) dashboard_parser.add_argument( "--assets-dir", help="Optional LoopX Chat web bundle directory. Defaults to packaged assets.", diff --git a/loopx/cli_commands/support_control_agent_runtime.py b/loopx/cli_commands/support_control_agent_runtime.py new file mode 100644 index 0000000000..c20850b002 --- /dev/null +++ b/loopx/cli_commands/support_control_agent_runtime.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import argparse + + +def register_agent_runtime_arguments( + parser: argparse.ArgumentParser, + *, + kiro_cli_bin: str, +) -> None: + """Register the shared Agent runtime selectors for Chat and Dashboard.""" + parser.add_argument( + "--codex-bin", + default="codex", + help="Codex CLI executable used for the read-only app-server session.", + ) + parser.add_argument( + "--claude-bin", + default="claude", + help="Claude Code CLI executable used for read-only Agent sessions.", + ) + parser.add_argument( + "--kiro-cli-bin", + default=kiro_cli_bin, + help="Kiro CLI executable used for read-only ACP Agent sessions (` acp`).", + ) + parser.add_argument( + "--lark-cli-bin", + help=( + "Optional explicit lark-cli executable. When omitted, LoopX uses its " + "bounded runtime discovery order." + ), + ) diff --git a/loopx/control_plane/todos/authoring_scope.ts b/loopx/control_plane/todos/authoring_scope.ts index 41d3800709..2e17a2c3d5 100644 --- a/loopx/control_plane/todos/authoring_scope.ts +++ b/loopx/control_plane/todos/authoring_scope.ts @@ -4,7 +4,11 @@ import type { JsonObject } from "../effect_program.ts"; import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; import { requireJsonObject } from "../runtime_decode.ts"; import { normalizeRegisteredTodoAgents, normalizeTodoAgent, stripPythonWhitespace } from "../coordination/todo_agents.ts"; -import { normalizeTodoResumeWhen, TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION } from "./resume_condition.ts"; +import { + normalizeTodoResumeWhen, + TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, + UNSUPPORTED_TODO_RESUME_CONDITION_MESSAGE, +} from "./resume_condition.ts"; export const TODO_AUTHORING_SCOPE_REQUEST_SCHEMA = "todo_authoring_scope_request_v0"; export const TODO_AUTHORING_SCOPE_RESULT_SCHEMA = "todo_authoring_scope_result_v0"; @@ -157,7 +161,7 @@ export function planTodoAuthoringScope(value: unknown): JsonObject { } const resume = intent.resume_when ? normalizeTodoResumeWhen({schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: intent.resume_when}) : null; - if (intent.resume_when && !resume) fail("unsupported Todo resume condition"); + if (intent.resume_when && !resume) fail(UNSUPPORTED_TODO_RESUME_CONDITION_MESSAGE); if (resume && intent.clear_resume_when) fail("todo update accepts either resume_when or clear_resume_when, not both"); const existingResume = todo.resume_when ? normalizeTodoResumeWhen({schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: todo.resume_when}) : null; diff --git a/loopx/control_plane/todos/field_update.ts b/loopx/control_plane/todos/field_update.ts index cff9664063..c47e8cf3cf 100644 --- a/loopx/control_plane/todos/field_update.ts +++ b/loopx/control_plane/todos/field_update.ts @@ -9,7 +9,12 @@ import { buildTodoCompletionMetadataUpdates, TODO_COMPLETION_STATE_REQUEST_SCHEMA, } from "./completion_state.ts"; -import { normalizeTodoResumeWhen, TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION } from "./resume_condition.ts"; +import { validateLegacyContinuationPolicyRepair } from "./legacy_continuation_policy_migration.ts"; +import { + normalizeTodoResumeWhen, + TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, + UNSUPPORTED_TODO_RESUME_CONDITION_MESSAGE, +} from "./resume_condition.ts"; import { MONITOR_METADATA_FIELDS, planMonitorMetadata, TODO_MONITOR_METADATA_REQUEST_SCHEMA } from "./monitor_metadata.ts"; export const TODO_FIELD_UPDATE_REQUEST_SCHEMA = "loopx_todo_field_update_request_v0"; @@ -74,19 +79,6 @@ function validateIntent(value: unknown): JsonObject { return intent; } -function validateRepair(block: JsonObject, intent: JsonObject, todoId: string): void { - const removed = stripPythonWhitespace(String(block.removed_continuation_policy ?? "")).toLowerCase(); - if (removed !== "primary_review" && removed !== "review_handoff") return; - const prefix = `todo_id '${todoId}' uses removed continuation_policy=${removed}; `; - if (intent.claim_only) throw new EffectRuntimeRequestError(prefix + "repair it before claiming"); - const repair = stripPythonWhitespace(String(intent.continuation_policy ?? "")).toLowerCase(); - const exclusions = Array.isArray(intent.excluded_agents) ? intent.excluded_agents : []; - if (repair !== "independent_handoff" || !exclusions.some(agent => existingAgent(agent) !== null)) { - throw new EffectRuntimeRequestError(prefix + - "repair it explicitly with continuation_policy=independent_handoff and excluded_agents="); - } -} - function bindingUpdates(block: JsonObject, intent: JsonObject, todoId: string): JsonObject { const updates: JsonObject = {}; if (intent.clear_claim) updates.claimed_by = null; @@ -150,11 +142,13 @@ export function planTodoFieldUpdate(value: unknown): TodoFieldUpdatePlan { const resumeWhen = intent.resume_when ? normalizeTodoResumeWhen({ schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: intent.resume_when, }) : null; - if (intent.resume_when && !resumeWhen) throw new EffectRuntimeRequestError("unsupported Todo resume condition"); + if (intent.resume_when && !resumeWhen) { + throw new EffectRuntimeRequestError(UNSUPPORTED_TODO_RESUME_CONDITION_MESSAGE); + } if (resumeWhen && intent.clear_resume_when) { throw new EffectRuntimeRequestError("todo update accepts either resume_when or clear_resume_when, not both"); } - validateRepair(block, intent, todoId); + validateLegacyContinuationPolicyRepair(block, intent, todoId); const status = intent.status ? stripPythonWhitespace(String(intent.status)).toLowerCase() : null; if (status !== null && !STATUS.includes(status as Status)) { throw new EffectRuntimeRequestError("todo status must be one of: open, done, blocked, deferred"); diff --git a/loopx/control_plane/todos/legacy_continuation_policy_migration.ts b/loopx/control_plane/todos/legacy_continuation_policy_migration.ts new file mode 100644 index 0000000000..5f3ca117b2 --- /dev/null +++ b/loopx/control_plane/todos/legacy_continuation_policy_migration.ts @@ -0,0 +1,43 @@ +/** Explicit migration boundary for removed hierarchical Todo continuation policies. */ +import type { JsonObject } from "../effect_program.ts"; +import { AuthorityStoreProtocolError } from "../coordination/authority_store_codec.ts"; +import { normalizeTodoAgent, stripPythonWhitespace } from "../coordination/todo_agents.ts"; +import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; + +function existingAgent(value: unknown): string | null { + try { + return normalizeTodoAgent(value, "agent_id"); + } catch (error) { + if (error instanceof AuthorityStoreProtocolError) return null; + throw error; + } +} + +export function validateLegacyContinuationPolicyRepair( + block: JsonObject, + intent: JsonObject, + todoId: string, +): void { + const removed = stripPythonWhitespace( + String(block.removed_continuation_policy ?? ""), + ).toLowerCase(); + if (removed !== "primary_review" && removed !== "review_handoff") return; + const prefix = `todo_id '${todoId}' uses removed continuation_policy=${removed}; `; + if (intent.claim_only) { + throw new EffectRuntimeRequestError(prefix + "repair it before claiming"); + } + const repair = stripPythonWhitespace( + String(intent.continuation_policy ?? ""), + ).toLowerCase(); + const exclusions = Array.isArray(intent.excluded_agents) ? intent.excluded_agents : []; + if ( + repair !== "independent_handoff" || + !exclusions.some((agent) => existingAgent(agent) !== null) + ) { + throw new EffectRuntimeRequestError( + prefix + + "repair it explicitly with continuation_policy=independent_handoff and " + + "excluded_agents=", + ); + } +} diff --git a/loopx/control_plane/todos/resume_condition.ts b/loopx/control_plane/todos/resume_condition.ts index fca195e398..7393d061ea 100644 --- a/loopx/control_plane/todos/resume_condition.ts +++ b/loopx/control_plane/todos/resume_condition.ts @@ -34,6 +34,11 @@ export const TODO_RESUME_KINDS = [ "monitor_changed", ] as const; +export const UNSUPPORTED_TODO_RESUME_CONDITION_MESSAGE = + "unsupported Todo resume condition; supported conditions are: " + + "todo_done:, monitor_changed:, " + + "pr_merged:[owner/repo]#, or capacity_available:"; + type TodoResumeKind = typeof TODO_RESUME_KINDS[number]; const TODO_ID_PATTERN = /^todo_[a-z\d_-]{3,64}$/; diff --git a/tests/control_plane_ts/todo_authoring_scope.test.ts b/tests/control_plane_ts/todo_authoring_scope.test.ts index 251f4e9f09..e914fc9396 100644 --- a/tests/control_plane_ts/todo_authoring_scope.test.ts +++ b/tests/control_plane_ts/todo_authoring_scope.test.ts @@ -86,6 +86,10 @@ test("deferred state requires a supported condition and clears remain explicit", const reopened = plan({status: "open", clear_resume_when: true}, {command: "update", role: "agent", todo}); assert.equal(reopened.effective_resume_when, null); assert.throws(() => plan({resume_when: "todo_done:todo_dependency", clear_resume_when: true}, {command: "update", role: "agent", todo}), /not both/); + assert.throws( + () => plan({resume_when: "free text"}, {command: "update", role: "agent", todo}), + /supported conditions are:.*todo_done:.*monitor_changed:.*pr_merged:.*capacity_available:/, + ); }); test("resolved successor scope is checked without using draft defaults", () => { diff --git a/tests/control_plane_ts/todo_field_update.test.ts b/tests/control_plane_ts/todo_field_update.test.ts index cc656fd0f7..95589bd5c0 100644 --- a/tests/control_plane_ts/todo_field_update.test.ts +++ b/tests/control_plane_ts/todo_field_update.test.ts @@ -46,7 +46,10 @@ test("resume generation belongs only to the monitor condition", () => { assert.equal(updates.resume_when, condition); assert.equal(updates.resume_monitor_generation, generation); } - assert.throws(() => plan({resume_when: "free text"}), /unsupported Todo resume/); + assert.throws( + () => plan({resume_when: "free text"}), + /supported conditions are:.*todo_done:.*monitor_changed:.*pr_merged:.*capacity_available:/, + ); assert.throws(() => plan({resume_when: "todo_done:todo_dependency", clear_resume_when: true}), /not both/); assert.throws(() => plan({status: "deferred", clear_resume_when: true}), /cannot clear/); }); From 48dba392025ed68cd3dd5ccae62a9fd029b79425 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 10 Sep 2026 07:18:13 +0800 Subject: [PATCH 3/5] test(smokes): align checks with current contracts Signed-off-by: huangruiteng --- examples/bootstrap-command-pack-smoke.py | 4 +++- .../peer-agent-hard-cut-boundary-smoke.py | 1 + examples/delivery-batch-scale-enum-smoke.py | 21 ++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/bootstrap-command-pack-smoke.py b/examples/bootstrap-command-pack-smoke.py index a3e0c06e27..52e8a07ccb 100644 --- a/examples/bootstrap-command-pack-smoke.py +++ b/examples/bootstrap-command-pack-smoke.py @@ -717,7 +717,9 @@ def test_skill_slash_fallback_contract() -> None: assert "pull_requests[review_action_kind!=null].review_template" in pr_review_skill_text assert "pull_requests[review_action_kind!=null].evidence_commands" in pr_review_skill_text assert "Do not pipe the only copy through `jq`" in pr_review_skill_text - assert "review_groups" in pr_review_skill_text + assert "`review_groups`" in pr_review_skill_text + assert "ranked actionable `review_sequence`" in pr_review_skill_text + assert "must not appear in `review_sequence`" in pr_review_skill_text assert "The five sections are output structure, while the execution contract is the evidence authority" in pr_review_normalized diff --git a/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py b/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py index 10482c8fc5..f6404e67d7 100644 --- a/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py +++ b/examples/control_plane/peer-agent-hard-cut-boundary-smoke.py @@ -31,6 +31,7 @@ ALLOWED_LEGACY_PATHS = { REPO_ROOT / "loopx" / "control_plane" / "agents" / "legacy_migration.py", REPO_ROOT / "loopx" / "control_plane" / "todos" / "contract.py", + REPO_ROOT / "loopx" / "control_plane" / "todos" / "legacy_continuation_policy_migration.ts", REPO_ROOT / "docs" / "reference" / "protocols" / "peer-agent-runtime-v1.md", REPO_ROOT / "docs" / "project-agent-todo-contract.md", REPO_ROOT / "docs" / "product" / "foundations" / "agent-profile-contract.md", diff --git a/examples/delivery-batch-scale-enum-smoke.py b/examples/delivery-batch-scale-enum-smoke.py index 705cd6f7d8..22301359d8 100644 --- a/examples/delivery-batch-scale-enum-smoke.py +++ b/examples/delivery-batch-scale-enum-smoke.py @@ -25,8 +25,10 @@ normalize_delivery_batch_scale, require_delivery_batch_scale, ) +from loopx.control_plane.work_items.delivery_history import ( # noqa: E402 + project_delivery_history, +) from loopx.state_refresh import refresh_state_run # noqa: E402 -from loopx.status import delivery_batch_scale_for_run # noqa: E402 GOAL_ID = "delivery-batch-scale-enum-fixture" @@ -205,9 +207,14 @@ def assert_refresh_state_enforces_enum(registry_path: Path) -> None: assert alias_payload["delivery_batch_scale"] == DeliveryBatchScale.SINGLE_SURFACE.value, alias_payload -def assert_status_uses_enum_not_raw_value() -> None: +def projected_delivery_batch_scale(run: dict[str, object]) -> str: + projection = project_delivery_history([run]) + return str(projection["runs"][0]["delivery_batch_scale"]) + + +def assert_delivery_history_uses_enum_not_raw_value() -> None: assert ( - delivery_batch_scale_for_run( + projected_delivery_batch_scale( { "classification": "runner_batch_fixture", "delivery_batch_scale": DeliveryBatchScale.MULTI_SURFACE.value, @@ -216,7 +223,7 @@ def assert_status_uses_enum_not_raw_value() -> None: == DeliveryBatchScale.MULTI_SURFACE.value ) assert ( - delivery_batch_scale_for_run( + projected_delivery_batch_scale( { "classification": "runner_batch_fixture", "delivery_batch_scale": "batch_plus_raw_logs", @@ -225,11 +232,11 @@ def assert_status_uses_enum_not_raw_value() -> None: == UNKNOWN_DELIVERY_BATCH_SCALE ) assert ( - delivery_batch_scale_for_run({"classification": "owner_handoff_consumer_test"}) + projected_delivery_batch_scale({"classification": "owner_handoff_consumer_test"}) == UNKNOWN_DELIVERY_BATCH_SCALE ) assert ( - delivery_batch_scale_for_run( + projected_delivery_batch_scale( { "classification": "owner_handoff_consumer_test", "delivery_batch_scale": DeliveryBatchScale.TEST_ONLY.value, @@ -244,7 +251,7 @@ def main() -> int: with tempfile.TemporaryDirectory(prefix="delivery-batch-scale-enum-") as tmp: registry_path, _runtime = write_fixture(Path(tmp)) assert_refresh_state_enforces_enum(registry_path) - assert_status_uses_enum_not_raw_value() + assert_delivery_history_uses_enum_not_raw_value() print("delivery batch scale enum smoke ok") return 0 From c0073d48638b10f9108f296a194e9b8944c7b672 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Fri, 11 Sep 2026 10:46:26 +0800 Subject: [PATCH 4/5] fix(release): restore exact-head qualification Signed-off-by: huangruiteng --- examples/blocker-push-runtime-smoke.py | 4 +-- ...ary-promotion-readiness-writeback-smoke.py | 1 + ...gent-onboard-host-loop-activation-smoke.py | 8 +++--- .../todo-standing-decision-authority-smoke.py | 4 ++- examples/issue-fix-acceptance-loop-smoke.py | 28 +++++++++++++++---- examples/reward-memory-dogfood-smoke.py | 3 ++ loopx/help_surface.py | 9 ++++++ man/loopx.1 | 6 ++++ 8 files changed, 50 insertions(+), 13 deletions(-) diff --git a/examples/blocker-push-runtime-smoke.py b/examples/blocker-push-runtime-smoke.py index f234afb4a0..6b26737bc8 100644 --- a/examples/blocker-push-runtime-smoke.py +++ b/examples/blocker-push-runtime-smoke.py @@ -212,8 +212,8 @@ def main() -> int: assert "Normal turns use CLI `interaction_contract`" in compact_prompt, prompt assert "`user_channel.notify` controls OUTPUT only" in compact_prompt, prompt assert "NOTIFY=向用户输出动作; DONT_NOTIFY=安静输出" in compact_prompt, prompt - assert "Due/peer gate != prompt" in compact_prompt, prompt - assert "missing NOTIFY action->具体user todo未投影" 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 assert "guard receipt; 2 stalls->replan" in compact_prompt, prompt assert "no-change=`surface_only`/no spend" in compact_prompt, prompt diff --git a/examples/canary/canary-promotion-readiness-writeback-smoke.py b/examples/canary/canary-promotion-readiness-writeback-smoke.py index 5175a8a7a9..c52d3ec414 100644 --- a/examples/canary/canary-promotion-readiness-writeback-smoke.py +++ b/examples/canary/canary-promotion-readiness-writeback-smoke.py @@ -116,6 +116,7 @@ def main() -> int: "LOOPX_RELEASES_DIR": str(root / "releases"), "LOOPX_SHELL_PROFILE": str(home / ".zshrc"), "LOOPX_REGISTRY": str(registry_path), + "LOOPX_PYTHON": sys.executable, "PYTHONPATH": str(REPO_ROOT), "SHELL": "/bin/zsh", } diff --git a/examples/control_plane/agent-onboard-host-loop-activation-smoke.py b/examples/control_plane/agent-onboard-host-loop-activation-smoke.py index 4933b82100..ff348482d6 100644 --- a/examples/control_plane/agent-onboard-host-loop-activation-smoke.py +++ b/examples/control_plane/agent-onboard-host-loop-activation-smoke.py @@ -312,7 +312,7 @@ def main() -> int: choice_run = subprocess.run( shlex.split(selected_choice["activation_input_command"]), cwd=REPO_ROOT, - env={**os.environ, "HOME": str(home)}, + env={**os.environ, "HOME": str(home), "LOOPX_PYTHON": sys.executable}, check=True, text=True, capture_output=True, @@ -415,7 +415,7 @@ def main() -> int: app_ssh_prompt_run = subprocess.run( shlex.split(app_ssh_activation["activation_input_command"]), cwd=REPO_ROOT, - env={**os.environ, "HOME": str(home)}, + env={**os.environ, "HOME": str(home), "LOOPX_PYTHON": sys.executable}, check=True, text=True, capture_output=True, @@ -466,7 +466,7 @@ def main() -> int: app_ssh_quota_run = subprocess.run( app_ssh_quota_argv, cwd=project, - env={**os.environ, "HOME": str(home)}, + env={**os.environ, "HOME": str(home), "LOOPX_PYTHON": sys.executable}, check=True, text=True, capture_output=True, @@ -493,7 +493,7 @@ def main() -> int: cli_onboarding["host_loop_activation"]["activation_input_command"] ), cwd=REPO_ROOT, - env={**os.environ, "HOME": str(home)}, + env={**os.environ, "HOME": str(home), "LOOPX_PYTHON": sys.executable}, check=True, text=True, capture_output=True, diff --git a/examples/control_plane/todo-standing-decision-authority-smoke.py b/examples/control_plane/todo-standing-decision-authority-smoke.py index a4cf1b7f43..842fceeb27 100755 --- a/examples/control_plane/todo-standing-decision-authority-smoke.py +++ b/examples/control_plane/todo-standing-decision-authority-smoke.py @@ -23,9 +23,11 @@ ) from loopx.control_plane.todos.decision_scope import ( # noqa: E402 build_required_decision_scope_consistency, - build_standing_decision_authority, standing_decision_authority_for_agent, ) +from loopx.control_plane.todos.standing_decision import ( # noqa: E402 + build_standing_decision_authority, +) from loopx.quota import build_quota_should_run # noqa: E402 GOAL_ID = "standing-decision-authority-fixture" diff --git a/examples/issue-fix-acceptance-loop-smoke.py b/examples/issue-fix-acceptance-loop-smoke.py index add15f8f6e..346bd29caa 100644 --- a/examples/issue-fix-acceptance-loop-smoke.py +++ b/examples/issue-fix-acceptance-loop-smoke.py @@ -2,7 +2,9 @@ from __future__ import annotations import json +import os import shlex +import shutil import subprocess import sys import tempfile @@ -45,6 +47,7 @@ def _run_caller_repo_json_command( *, issue_branch: str = "codex/issue-123-public-metadata-fixture", expect_success: bool = True, + env: dict[str, str] | None = None, ) -> dict[str, Any]: result = subprocess.run( [ @@ -74,6 +77,7 @@ def _run_caller_repo_json_command( stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=expect_success, + env=env, ) if not expect_success and result.returncode == 0: raise AssertionError("caller repo branch mode unexpectedly succeeded") @@ -346,18 +350,30 @@ def main() -> int: _run_git(repo_path, ["commit", "-m", "Prepare a later base revision"]) later_revision = _git_output(repo_path, ["rev-parse", "HEAD"]) _run_git(repo_path, ["checkout", "-b", "caller-start", "main"]) - hook = repo_path / ".git" / "hooks" / "post-checkout" - hook.write_text( + real_git = shutil.which("git") + assert real_git is not None + shim_dir = fixture_root / "git-shim" + shim_dir.mkdir() + git_shim = shim_dir / "git" + git_shim.write_text( "#!/bin/sh\n" - f"git update-ref refs/heads/main {later_revision}\n", + f"{shlex.quote(real_git)} \"$@\"\n" + "status=$?\n" + "if [ \"$status\" -eq 0 ] && [ \"$1\" = checkout ] && " + "[ \"$2\" = -b ] && [ \"$3\" = codex/issue-123-pinned-base ]; then\n" + f" {shlex.quote(real_git)} update-ref refs/heads/main {later_revision}\n" + "fi\n" + "exit \"$status\"\n", encoding="utf-8", ) - hook.chmod(0o755) - _run_git(repo_path, ["config", "core.hooksPath", ".git/hooks"]) - + git_shim.chmod(0o755) pinned_payload = _run_caller_repo_json_command( repo_path, issue_branch="codex/issue-123-pinned-base", + env={ + **os.environ, + "PATH": str(shim_dir) + os.pathsep + os.environ.get("PATH", ""), + }, ) pinned_branch = pinned_payload["caller_repo_branch"] assert pinned_branch["base_snapshot"]["base_revision"] == approved_revision diff --git a/examples/reward-memory-dogfood-smoke.py b/examples/reward-memory-dogfood-smoke.py index e17e0b543d..1ac111d382 100644 --- a/examples/reward-memory-dogfood-smoke.py +++ b/examples/reward-memory-dogfood-smoke.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import os import sys import json import subprocess @@ -582,6 +583,7 @@ def main() -> None: check=True, capture_output=True, text=True, + env={**os.environ, "LOOPX_PYTHON": sys.executable}, ) cli_packet = json.loads(completed.stdout) assert cli_packet["status"] == "ready_for_bounded_issue_fix_pilot" @@ -631,6 +633,7 @@ def main() -> None: check=True, capture_output=True, text=True, + env={**os.environ, "LOOPX_PYTHON": sys.executable}, ) control_packet = json.loads(completed.stdout) assert control_packet["status"] == "control_ready" diff --git a/loopx/help_surface.py b/loopx/help_surface.py index fe44c29675..918f042d21 100644 --- a/loopx/help_surface.py +++ b/loopx/help_surface.py @@ -83,6 +83,14 @@ "command": "loopx goal-channel --help", "purpose": "Set up, inspect, sync, or notify the provider channel bound to one goal.", }, + { + "command": "loopx goal-portfolio --help", + "purpose": "Read bounded, source-covered evidence across explicitly selected Goals.", + }, + { + "command": "loopx manager-inbox --help", + "purpose": "Read source-bound Manager context and record the receiving Agent's decision.", + }, { "command": "loopx goal-lifecycle --help", "purpose": "Preview, stop, or resume a Goal without deleting its history, todos, or evidence.", @@ -295,6 +303,7 @@ MANPAGE_COMMAND_HELP_ONLY = frozenset( { "archive-runtime", + "automation-prompts", "authority-shadow", "backup-state", "capability", diff --git a/man/loopx.1 b/man/loopx.1 index ba3638a58e..e46eeb0019 100644 --- a/man/loopx.1 +++ b/man/loopx.1 @@ -75,6 +75,12 @@ Render a handoff or review packet with any required evidence\-log reads. \fBloopx goal\-channel \-\-help\fR Set up, inspect, sync, or notify the provider channel bound to one goal. .TP +\fBloopx goal\-portfolio \-\-help\fR +Read bounded, source\-covered evidence across explicitly selected Goals. +.TP +\fBloopx manager\-inbox \-\-help\fR +Read source\-bound Manager context and record the receiving Agent's decision. +.TP \fBloopx goal\-lifecycle \-\-help\fR Preview, stop, or resume a Goal without deleting its history, todos, or evidence. .TP From 0496975e46e3c8ebd87c8b2bc1eed59b3fa4e3c7 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Fri, 11 Sep 2026 11:09:10 +0800 Subject: [PATCH 5/5] test(cli): classify manager read surfaces Signed-off-by: huangruiteng --- loopx/control_plane/testing/cli_output_budget.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/loopx/control_plane/testing/cli_output_budget.py b/loopx/control_plane/testing/cli_output_budget.py index 3cefecde30..786cfa4105 100644 --- a/loopx/control_plane/testing/cli_output_budget.py +++ b/loopx/control_plane/testing/cli_output_budget.py @@ -657,6 +657,18 @@ class CliOutputCommandClassification: surface_id=None, rationale="explicit provider setup, sync, doctor, and gate-notification command family", ), + CliOutputCommandClassification( + command_id="goal-portfolio", + qualification="explicit_cold_path_exception", + surface_id=None, + rationale="explicit manager-scoped evidence read with caller-provided goal bounds", + ), + CliOutputCommandClassification( + command_id="manager-inbox", + qualification="explicit_cold_path_exception", + surface_id=None, + rationale="explicit manager-context read and acknowledgement command family", + ), CliOutputCommandClassification( command_id="goal-lifecycle", qualification="explicit_cold_path_exception",