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
4 changes: 2 additions & 2 deletions apps/presentation/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ It provides a unified, coherent experience for managing long-running agent Goals

```bash
loopx goal-lifecycle --goal-id <goal-id> --operation stop
loopx goal-lifecycle --goal-id <goal-id> --operation stop --execute
loopx goal-lifecycle --goal-id <goal-id> --operation resume --execute
loopx goal-lifecycle --goal-id <goal-id> --operation stop --actor-kind owner --execute
loopx goal-lifecycle --goal-id <goal-id> --operation resume --actor-kind owner --execute
loopx quota status --goal-id <goal-id>
```

Expand Down
5 changes: 4 additions & 1 deletion docs/book/chapters/workspace-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ Preview 冻结规范化参数、影响范围和当前 revision。Apply 只能执

```bash
loopx goal-lifecycle --goal-id <goal-id> --operation stop
loopx goal-lifecycle --goal-id <goal-id> --operation stop --execute
loopx goal-lifecycle --goal-id <goal-id> --operation stop --actor-kind owner --execute
loopx quota status --goal-id <goal-id>
```

执行 lifecycle transition 时必须显式传入 `--actor-kind owner` 或 `controller`;
匿名预览仍然保持只读。

暂停会让该 Goal 退出 active attention,并使有效自动运行 quota 投影为 0;Todo、历史、证据和配置
仍保留。恢复使用显式 `resume --execute`,且不会绕过 Todo、Gate 或 quota。不要把 stop 写成
“完成 Goal”,也不要用改 quota 的方式意外恢复一个被 owner 停止的 Goal。
Expand Down
5 changes: 4 additions & 1 deletion docs/book/en/chapters/workspace-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ For example, the first Goal-stop command is preview-only:

```bash
loopx goal-lifecycle --goal-id <goal-id> --operation stop
loopx goal-lifecycle --goal-id <goal-id> --operation stop --execute
loopx goal-lifecycle --goal-id <goal-id> --operation stop --actor-kind owner --execute
loopx quota status --goal-id <goal-id>
```

Executed lifecycle transitions require an explicit `--actor-kind owner` or
`controller`; anonymous previews remain read-only.

Stopping a Goal removes it from active attention and projects zero effective automatic-run quota while
preserving Todos, history, evidence, and configuration. Explicit `resume --execute` restores scheduling
eligibility but does not bypass Todo, Gate, or quota rules. Do not describe stop as completing the Goal, and
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/codex-app-autonomous-goal-experience.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ loopx history --goal-id "$GOAL_ID"

```bash
loopx goal-lifecycle --goal-id "$GOAL_ID" --operation stop
loopx goal-lifecycle --goal-id "$GOAL_ID" --operation stop --execute
loopx goal-lifecycle --goal-id "$GOAL_ID" --operation stop --actor-kind owner --execute
```

**预期效果:**
Expand All @@ -225,7 +225,7 @@ loopx goal-lifecycle --goal-id "$GOAL_ID" --operation stop --execute
**输入:**

```bash
loopx goal-lifecycle --goal-id "$GOAL_ID" --operation resume --execute
loopx goal-lifecycle --goal-id "$GOAL_ID" --operation resume --actor-kind owner --execute
loopx quota should-run \
--goal-id "$GOAL_ID" \
--agent-id "$AGENT_ID" \
Expand Down
1 change: 1 addition & 0 deletions docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ loopx operator-gate \

loopx reward \
--goal-id your-project-goal \
--actor-kind owner \
--decision continue_route \
--reward positive \
--reason-summary "validation improved and the route is worth extending"
Expand Down
7 changes: 5 additions & 2 deletions docs/guides/personal-workspace-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,16 @@ CLI 提供同一套可预览、可验证的生命周期操作:
loopx goal-lifecycle --goal-id <goal-id> --operation stop

# 确认执行,再读取 quota 验证自动推进已暂停
loopx goal-lifecycle --goal-id <goal-id> --operation stop --execute
loopx goal-lifecycle --goal-id <goal-id> --operation stop --actor-kind owner --execute
loopx quota status --goal-id <goal-id>

# 恢复;不会绕过其他运行门禁
loopx goal-lifecycle --goal-id <goal-id> --operation resume --execute
loopx goal-lifecycle --goal-id <goal-id> --operation resume --actor-kind owner --execute
```

`--execute` 必须显式声明 `--actor-kind owner` 或 `controller`;不带 actor 的
预览仍保持只读。写入的 activation receipt 会保留该 actor kind。

执行时,LoopX 会写入权威 source registry、同步全局 registry,并验证两端 readback;任一端未验证成功时不会宣称操作完成。

切换到 SSH 状态来源后,只有来源与本机 OpenSSH 配置中的精确 Host alias 绑定时,
Expand Down
4 changes: 3 additions & 1 deletion docs/heartbeat-automation-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,9 @@ hint directly, otherwise do nothing. For the uniquely matched current heartbeat,

If `automation_update` is unavailable in the session and
`scheduler_hint.app_automation.fallback_hint.available=true`, run the bound
`fallback_hint.cli_args` (`loopx-apply-rrule`) once instead. It backs up
`fallback_hint.cli_args` (`loopx-apply-rrule`) once instead. The fallback
requires the projected registered `--agent-id`; there is no implicit Agent
default. It backs up
`codex-dev.db`, syncs the automation TOML and SQLite row, and runs the bound
ACK; direct SQLite edits bypass the app API, so this is a bounded fallback and
never the routine path. The bridge reuses the provided parent Turn for its
Expand Down
7 changes: 7 additions & 0 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,12 +573,17 @@ editing the run JSON by hand:
```bash
loopx reward \
--goal-id project-goal \
--actor-kind owner \
--decision continue_route \
--reward positive \
--reason-summary "comparable validation improved and the route is worth extending" \
--follow-up "promote to the next longer-window check"
```

Durable reward writes require an explicit `--actor-kind owner` or
`--actor-kind controller`; `--dry-run` remains available without an actor.
The selected kind is stored with the run-bound overlay.

By default the command attaches feedback to the latest compact run for the
goal. Pass `--run-generated-at <timestamp>` to target an older run. The writer
appends a JSONL overlay to the same `index.jsonl`; it does not mutate private
Expand All @@ -604,6 +609,7 @@ overlay instead of creating a separate memory store:
```bash
loopx reward \
--goal-id project-goal \
--actor-kind owner \
--decision route_correction \
--reward mixed \
--reason-summary "fix lifecycle counters before adding more benchmark cases" \
Expand All @@ -629,6 +635,7 @@ the durable loop in one CLI call:
```bash
loopx reward \
--goal-id project-goal \
--actor-kind owner \
--decision continue_route \
--reward positive \
--reason-summary "comparable validation improved and the route is worth extending" \
Expand Down
1 change: 1 addition & 0 deletions docs/product/roadmaps/experiment-controller-milestone.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Use `loopx reward` to append this compact signal to an existing run:
```bash
loopx reward \
--goal-id example-experiment-goal \
--actor-kind owner \
--run-generated-at 2026-06-01T00:00:00+00:00 \
--decision continue_route \
--reward positive \
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/contracts/dashboard-reward-write-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ A browser append endpoint may be implemented only when all of these are true:
- The payload has already passed the same validation as `/reward/dry-run`.
- The response remains compact and does not return `index_path`, `json_path`,
`markdown_path`, local absolute paths, or raw private evidence.
- The trusted loopback adapter records `actor_kind=owner` in both preview and
append receipts; the canonical CLI requires an explicit owner/controller
actor kind for a durable write.

## Preview Handshake

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Browser append is allowed only when all of these are true:

Successful append writes one run-bound `human_reward` overlay row. Active state
may carry a summary, but the run overlay remains the durable source of truth.
The CLI requires `--actor-kind owner|controller`; the opt-in loopback adapter
records `owner` for its reviewed preview/apply path.

## Operator Gate

Expand Down
9 changes: 6 additions & 3 deletions docs/status-data-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -2000,9 +2000,9 @@ quiet skip.
For `controller_readiness`, the status export keeps only controller-stage
booleans, missing gate names, operator-facing review text, next handoff
condition, and compact gate rows with `id`, `ok`, and `review`. For
`human_reward`, the status export keeps only `recorded_at`, `decision`,
`reward`, `reason_summary`, and `follow_up`. For `operator_gate`, the status
export keeps only `recorded_at`, `gate`, `decision`, `operator_question`,
`human_reward`, the status export keeps only `recorded_at`, `actor_kind`,
`decision`, `reward`, `reason_summary`, and `follow_up`. For `operator_gate`,
the status export keeps only `recorded_at`, `gate`, `decision`, `operator_question`,
`reason_summary`, `follow_up`, and `agent_command`. Operator-gate runs may also
include a compact `operator_gate_resume_contract` with
`version=operator_gate_resume_contract_v0`, `gate_id`, `created_state_ref`,
Expand Down Expand Up @@ -2040,6 +2040,7 @@ Operators can append `human_reward` with the CLI:
```bash
loopx reward \
--goal-id example-experiment-goal \
--actor-kind owner \
--decision continue_route \
--reward positive \
--reason-summary "comparable validation improved and the route is worth extending"
Expand All @@ -2055,6 +2056,7 @@ operating-rule correction, the overlay may also include a compact lesson:
```bash
loopx reward \
--goal-id example-experiment-goal \
--actor-kind owner \
--decision route_correction \
--reward mixed \
--reason-summary "run the driver repair before expanding cases" \
Expand Down Expand Up @@ -2105,6 +2107,7 @@ operator explicitly asks for it:
```bash
loopx reward \
--goal-id example-experiment-goal \
--actor-kind owner \
--decision continue_route \
--reward positive \
--reason-summary "comparable validation improved and the route is worth extending" \
Expand Down
67 changes: 65 additions & 2 deletions examples/cli-project-lifecycle-command-modularization-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / "loopx" / "cli.py"
MODULE = ROOT / "loopx" / "cli_commands" / "project_lifecycle.py"
PROJECT_SKILL = ROOT / "skills" / "loopx-project" / "SKILL.md"
# The `refresh-state` command owns its own module since #4521, so the markers
# that belong to that command are required there instead of in the dispatcher.
REFRESH_MODULE = ROOT / "loopx" / "cli_commands" / "project_lifecycle_refresh_state.py"
Expand Down Expand Up @@ -164,6 +165,18 @@ def main() -> None:
cli_source = CLI.read_text(encoding="utf-8")
module_source = MODULE.read_text(encoding="utf-8")
init_source = INIT.read_text(encoding="utf-8")
project_skill = PROJECT_SKILL.read_text(encoding="utf-8")
reward_skill = project_skill.split("## Record Human Reward", 1)[1].split(
"## Multi-Project Status", 1
)[0]

for marker in (
"--dry-run",
"--actor-kind owner",
"--actor-kind controller",
"Never infer",
):
require(marker in reward_skill, f"project skill reward flow omitted {marker}")

forbidden_cli_markers = [
"refresh_state_parser = sub.add_parser",
Expand Down Expand Up @@ -218,7 +231,7 @@ def main() -> None:
"--dry-run",
),
"read-only-map": ("--recommended-action", "--dry-run"),
"reward": ("--write-active-state-summary", "--lesson-kind", "--lesson-avoid", "--dry-run"),
"reward": ("--actor-kind", "--write-active-state-summary", "--lesson-kind", "--lesson-avoid", "--dry-run"),
"operator-gate": ("--agent-command", "--no-global-sync"),
}.items():
help_text = require_success(run_cli(command, "--help"))
Expand Down Expand Up @@ -320,6 +333,53 @@ def main() -> None:
"reward lesson avoid changed",
)

rejected_reward = run_cli(
*command_prefix,
"reward",
"--goal-id",
GOAL_ID,
"--decision",
"continue_route",
"--reward",
"positive",
"--reason-summary",
"synthetic durable reward",
"--format",
"json",
)
require(rejected_reward.returncode == 1, "anonymous durable reward should fail")
rejected_payload = json.loads(rejected_reward.stdout)
require(
"actor kind is required" in str(rejected_payload.get("error") or ""),
f"anonymous durable reward returned the wrong error: {rejected_payload}",
)
require(index_path.read_text(encoding="utf-8") == before_index, "rejected reward mutated run index")

durable_reward = require_json_success(
run_cli(
*command_prefix,
"reward",
"--goal-id",
GOAL_ID,
"--actor-kind",
"owner",
"--decision",
"continue_route",
"--reward",
"positive",
"--reason-summary",
"synthetic durable reward",
"--format",
"json",
)
)
require(durable_reward.get("actor_kind") == "owner", "durable reward lost its actor")
persisted_reward = json.loads(index_path.read_text(encoding="utf-8").splitlines()[-1])
require(
(persisted_reward.get("human_reward") or {}).get("actor_kind") == "owner",
"durable reward index row lost its actor",
)

gate_payload = require_json_success(
run_cli(
*command_prefix,
Expand All @@ -344,7 +404,10 @@ def main() -> None:
(gate_payload.get("operator_gate") or {}).get("decision") == "defer",
"operator-gate decision changed",
)
require(index_path.read_text(encoding="utf-8") == before_index, "dry-run commands mutated run index")
require(
len(index_path.read_text(encoding="utf-8").splitlines()) == 2,
"project lifecycle smoke wrote an unexpected number of run rows",
)

print("cli-project-lifecycle-command-modularization-smoke: ok")

Expand Down
3 changes: 2 additions & 1 deletion loopx/chat_goal_lifecycle_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def _apply_goal_lifecycle(
goal_id=goal_id,
state=target_state,
reason=parameters.get("reason"),
actor_kind="owner",
execute=True,
)
if not result.get("ok") or not (result.get("readback") or {}).get(
Expand Down Expand Up @@ -159,4 +160,4 @@ def _apply_goal_lifecycle(
),
receipt=receipt,
)
return {"proposal": stored, "turn": None}
return {"proposal": stored, "turn": None}
11 changes: 11 additions & 0 deletions loopx/cli_commands/goal_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections.abc import Callable
from pathlib import Path

from ..control_plane.actor_identity import OWNER_CONTROLLER_ACTOR_CHOICES
from ..control_plane.goals.activation_service import (
render_goal_activation_markdown,
set_goal_activation_state,
Expand Down Expand Up @@ -34,6 +35,14 @@ def register_goal_lifecycle_command(
help="Stop automatic advancement or restore eligibility.",
)
parser.add_argument("--reason", help="Bounded owner-visible transition reason.")
parser.add_argument(
"--actor-kind",
choices=OWNER_CONTROLLER_ACTOR_CHOICES,
help=(
"Explicit non-Agent actor for --execute. Anonymous preview remains "
"available when this option is omitted."
),
)
parser.add_argument(
"--expected-state-fingerprint",
help="SHA-256 registry fingerprint from a fresh goal-actions projection.",
Expand All @@ -59,6 +68,7 @@ def handle_goal_lifecycle_command(
reason=args.reason,
runtime_root_override=args.runtime_root,
expected_state_fingerprint=args.expected_state_fingerprint,
actor_kind=args.actor_kind,
execute=bool(args.execute),
)
except Exception as exc:
Expand All @@ -68,6 +78,7 @@ def handle_goal_lifecycle_command(
"dry_run": not bool(args.execute),
"execute": bool(args.execute),
"goal_id": args.goal_id,
"actor_kind": args.actor_kind,
"changed": False,
"written": False,
"error": str(exc),
Expand Down
10 changes: 10 additions & 0 deletions loopx/cli_commands/project_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..control_plane.capability_hooks import (
PostWritebackHookRegistration,
)
from ..control_plane.actor_identity import OWNER_CONTROLLER_ACTOR_CHOICES
from ..feedback import (
LESSON_KINDS,
append_human_reward,
Expand Down Expand Up @@ -103,6 +104,14 @@ def register_project_lifecycle_commands(
help="Exact run generated_at timestamp. Defaults to the latest compact run for the goal.",
)
reward_parser.add_argument("--recorded-at", help="Reward timestamp. Defaults to current UTC time.")
reward_parser.add_argument(
"--actor-kind",
choices=OWNER_CONTROLLER_ACTOR_CHOICES,
help=(
"Explicit non-Agent actor for the durable append. Anonymous dry-run "
"remains available when this option is omitted."
),
)
reward_parser.add_argument("--decision", required=True, help="Operator decision label, such as continue_route.")
reward_parser.add_argument(
"--reward",
Expand Down Expand Up @@ -265,6 +274,7 @@ def handle_project_lifecycle_command(
goal_id=args.goal_id,
run_generated_at=args.run_generated_at,
reward=reward,
actor_kind=args.actor_kind,
dry_run=bool(args.dry_run),
state_file_override=Path(args.state_file).expanduser() if args.state_file else None,
write_active_state_summary=bool(args.write_active_state_summary),
Expand Down
Loading
Loading