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
Original file line number Diff line number Diff line change
Expand Up @@ -729,9 +729,10 @@ promotion retain their own acceptance. No new paid cohort or soak is authorized.
1. **Supported package/profile matrix:** release and host owners pin all supported
object-only, codec-only and enforcement packages and exclusion evidence before
M2 activation. v2 names are proposed; protocol support requires semantics.
2. **Exact commit guard:** Goal lifecycle and TS transaction owners must choose
and prove the local retirement/commit lock and external-effect drain contract
before M2. A digest recheck alone cannot discharge this hold.
2. **Exact commit guard:** The M2 source-session candidate uses one
alias-scoped cross-runtime lock around the project-registry transaction.
M3 must still prove the external-effect drain contract before activation.
A digest recheck alone cannot discharge that hold.
3. **Canonical destination/provider import:** reuse the current path owner; follow
#4915 without assuming merge. Provider-state adoption needs its own reviewed
import contract; the first file-only slice rejects it.
Expand All @@ -755,6 +756,18 @@ promotion retain their own acceptance. No new paid cohort or soak is authorized.
- **Effect on normative design:** Align with roadmap/TS/shared authority; separate codec
compatibility from enforcement, specify commit fencing and legacy cleanup.

### 2026-09-23: M2 source-session implementation candidate

- **Baseline:** `cbbdd837f65c8ba28161115cc4ce39093bfa2951`
- **Proposed:** A fresh-project-only `source_session_v1` profile, exact bind and
unbind receipts, journaled A-to-B recreation, and read-only exact resolution.
- **Evidence:** Real CLI tests cover ABA ordering, pre-publication retry,
post-publication forward repair, exact-byte replacement rollback, operation
ID conflicts, capacity rejection, and replay at capacity.
- **Remaining hold:** Every result has `execution_authority: false`. M3 must
qualify the remaining effect owners before existing-project activation or
global routing can open.

## Appendix B: Decision log

| Date | Decision | Owner / approval | Alternatives | Normative sections changed |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,9 +661,9 @@ service adoption、D1–D3 provider promotion 保留各自验收。不授权付
1. **受支持 package/profile matrix:** release/host owner 在 M2 activation 前固定
object-only、codec-only、enforcement 版本及排除证据。v2 名称是提案,support
必须包含语义。
2. **精确 commit guard:** Goal lifecycle/TS transaction owner 在 M2 前选定并证明
本地 retirement/commit lock 与 external-effect drain contract;digest recheck
不能解除 hold。
2. **精确 commit guard:** M2 source-session 候选在 project-registry transaction
外使用 alias-scoped cross-runtime lock。M3 仍须在 activation 前证明
external-effect drain contract;digest recheck 不能解除该 hold。
3. **Canonical destination/provider import:** 复用当前 path owner,跟随 #4915
但不预设已合并。Provider-state adoption 要有独立审阅的 import contract;首个
file-only 切片拒绝它。
Expand All @@ -687,6 +687,16 @@ service adoption、D1–D3 provider promotion 保留各自验收。不授权付
- **对规范设计的影响:** 对齐 roadmap/TS/shared authority;分开 codec compatibility
与 enforcement,明确 commit fence、legacy cleanup。

### 2026-09-23:M2 source-session 实现候选

- **基线:** `cbbdd837f65c8ba28161115cc4ce39093bfa2951`
- **候选实现:** 仅支持新项目的 `source_session_v1` profile、精确 bind/unbind
receipt、带 journal 的 A-to-B recreation,以及只读精确 resolution。
- **证据:** 真实 CLI 测试覆盖 ABA 顺序、publication 前重试、publication 后前向
修复、replacement 原字节回滚、operation ID 冲突、容量拒绝和满容量 replay。
- **剩余 hold:** 所有结果均为 `execution_authority: false`。M3 必须先完成其余
effect owner 资格化,才能开放既有项目 activation 或 global routing。

## 附录 B:决策日志

| 日期 | 决策 | Owner/批准 | 替代方案 | 变更的规范章节 |
Expand Down
15 changes: 11 additions & 4 deletions loopx/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .control_plane.projects.registry_codec import (
load_project_registry,
mutate_project_registry,
require_runtime_compatible_project_registry,
)
from .control_plane.runtime.time import now_local_iso
from .public_safe_text import (
Expand Down Expand Up @@ -457,9 +458,12 @@ def reduce(
)

if dry_run:
summary, previous_entry = reduce(
copy.deepcopy(load_project_registry(registry_path))
registry = load_project_registry(registry_path)
require_runtime_compatible_project_registry(
registry,
operation="authority source registration",
)
summary, previous_entry = reduce(copy.deepcopy(registry))
else:
summary, previous_entry = mutate_project_registry(
registry_path,
Expand Down Expand Up @@ -568,9 +572,12 @@ def reduce(
)

if dry_run:
summary, previous_entry = reduce(
copy.deepcopy(load_project_registry(registry_path))
registry = load_project_registry(registry_path)
require_runtime_compatible_project_registry(
registry,
operation="authority registry import",
)
summary, previous_entry = reduce(copy.deepcopy(registry))
else:
summary, previous_entry = mutate_project_registry(
registry_path,
Expand Down
8 changes: 7 additions & 1 deletion loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .control_plane.projects.registry_codec import (
load_project_registry,
project_registry_transaction,
require_runtime_compatible_project_registry,
)
from typing import Any

Expand Down Expand Up @@ -70,7 +71,12 @@ def now_iso() -> str:
def read_json_if_exists(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
return load_project_registry(path)
payload = load_project_registry(path)
require_runtime_compatible_project_registry(
payload,
operation="bootstrap",
)
return payload


def resolve_project_path(project: Path, path: Path | None) -> Path | None:
Expand Down
11 changes: 10 additions & 1 deletion loopx/claude_goal_mode/scripts/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from pathlib import Path

from loopx.control_plane.projects.registry_codec import (
ProjectRegistryProtocolError,
add_project_registry_backend,
load_project_registry,
require_runtime_compatible_project_registry,
)

PLUGIN_ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -70,10 +72,17 @@ def main():
reg = proj / ".goal-harness" / "registry.json"
if reg.exists():
try:
load_project_registry(reg)
payload = load_project_registry(reg)
require_runtime_compatible_project_registry(
payload,
operation="Claude Goal adapter",
)
print(f"[registry] mark agent_backends += claude ({reg})")
if not dry:
add_project_registry_backend(reg, "claude")
except ProjectRegistryProtocolError as e:
print(f"[registry] FAILED: {e}")
sys.exit(1)
except Exception as e:
print(" (registry annotate skipped:", e, ")")
else:
Expand Down
40 changes: 39 additions & 1 deletion loopx/cli_commands/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ..control_plane.projects.registry import (
PROJECT_KINDS,
bind_session,
recreate_goal,
register_project_goal,
resolve_project,
unbind_session,
Expand Down Expand Up @@ -44,6 +45,11 @@ def register_project_commands(
register_parser.add_argument("--stop-condition", required=True)
register_parser.add_argument("--repository", action="append", default=[])
register_parser.add_argument("--external-locator", action="append", default=[])
register_parser.add_argument(
"--goal-instance-profile",
choices=("source_session_v1",),
)
register_parser.add_argument("--operation-id")

bind_parser = project_sub.add_parser(
"bind-session",
Expand All @@ -52,6 +58,8 @@ def register_project_commands(
add_subcommand_format(bind_parser)
bind_parser.add_argument("--session-id", required=True)
bind_parser.add_argument("--goal-id", required=True)
bind_parser.add_argument("--goal-instance-id")
bind_parser.add_argument("--operation-id")

unbind_parser = project_sub.add_parser(
"unbind-session",
Expand All @@ -60,6 +68,18 @@ def register_project_commands(
add_subcommand_format(unbind_parser)
unbind_parser.add_argument("--session-id", required=True)
unbind_parser.add_argument("--goal-id", required=True)
unbind_parser.add_argument("--goal-instance-id")
unbind_parser.add_argument("--operation-id")

recreate_parser = project_sub.add_parser(
"recreate-goal",
help="Retire one exact Goal instance and publish its reserved successor.",
)
add_subcommand_format(recreate_parser)
recreate_parser.add_argument("--goal-id", required=True)
recreate_parser.add_argument("--goal-instance-id", required=True)
recreate_parser.add_argument("--operation-id", required=True)
recreate_parser.add_argument("--execute", action="store_true")

resolve_parser = project_sub.add_parser(
"resolve",
Expand All @@ -68,6 +88,8 @@ def register_project_commands(
add_subcommand_format(resolve_parser)
resolve_parser.add_argument("--project-id")
resolve_parser.add_argument("--session-id")
resolve_parser.add_argument("--goal-id")
resolve_parser.add_argument("--goal-instance-id")
resolve_parser.add_argument("--repository")
resolve_parser.add_argument("--external-locator")

Expand Down Expand Up @@ -120,26 +142,42 @@ def handle_project_command(
stop_condition=args.stop_condition,
repository_bindings=args.repository,
external_locator_bindings=args.external_locator,
goal_instance_profile=args.goal_instance_profile,
operation_id=args.operation_id,
)
elif args.project_command == "bind-session":
payload = bind_session(
registry_path=registry_path,
session_id=args.session_id,
goal_id=args.goal_id,
goal_instance_id=args.goal_instance_id,
operation_id=args.operation_id,
)
elif args.project_command == "unbind-session":
payload = unbind_session(
registry_path=registry_path,
session_id=args.session_id,
goal_id=args.goal_id,
goal_instance_id=args.goal_instance_id,
operation_id=args.operation_id,
)
else:
elif args.project_command == "resolve":
payload = resolve_project(
registry_path=registry_path,
explicit_project_id=args.project_id,
session_id=args.session_id,
repository=args.repository,
external_locator=args.external_locator,
goal_id=args.goal_id,
goal_instance_id=args.goal_instance_id,
)
else:
payload = recreate_goal(
registry_path=registry_path,
goal_id=args.goal_id,
goal_instance_id=args.goal_instance_id,
operation_id=args.operation_id,
execute=args.execute,
)
except (OSError, TypeError, ValueError, LegacyCoordinationWriterFenced, ShadowManagementError) as exc:
payload = {
Expand Down
5 changes: 5 additions & 0 deletions loopx/configure_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ProjectRegistryTransaction,
load_project_registry,
project_registry_transaction,
require_runtime_compatible_project_registry,
)
from .control_plane.todos.contract import normalize_todo_claimed_by
from .control_plane.todos.mutation_authority import (
Expand Down Expand Up @@ -723,6 +724,10 @@ def configure_goal(
if _registry_transaction is not None
else load_project_registry(registry_path)
)
require_runtime_compatible_project_registry(
payload,
operation="Goal configuration",
)
goals = registry_goals(payload)
goal = next((item for item in goals if str(item.get("id")) == goal_id), None)
if goal is None:
Expand Down
8 changes: 8 additions & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ import { projectVisionWaitCoverage } from "./goals/vision_wait_coverage.ts";
import { admitGoalAmendmentProposal } from "./goals/goal_amendment_proposal.ts";
import { projectSharedGoalAlignment } from "./goals/shared_goal_alignment.ts";
import { projectGoalOperatorActions } from "./goals/operator_actions.ts";
import {
decideGoalRecreation,
decideProjectSessionBind,
decideProjectSessionUnbind,
} from "./goals/source_session_lifetime.ts";
import {
evaluateDeliveryRoute,
} from "./turn_driver/delivery_continuity.ts";
Expand Down Expand Up @@ -515,6 +520,9 @@ export function createEffectRuntimeHandlers(
["goal.shared_goal_alignment.project", projectSharedGoalAlignment],
["goal.operator_actions.project", projectGoalOperatorActions],
["goal.amendment_proposal.admit", admitGoalAmendmentProposal],
["goal.source_session.bind.decide", decideProjectSessionBind],
["goal.source_session.unbind.decide", decideProjectSessionUnbind],
["goal.source_session.recreate.decide", decideGoalRecreation],
["goal.acceptance.inspect", inspectLocalGoalAcceptance],
["goal.acceptance.configure", commitLocalGoalAcceptance],
["goal.acceptance.verify.commit", commitLocalGoalAcceptanceVerification],
Expand Down
47 changes: 41 additions & 6 deletions loopx/control_plane/goals/goal_instance_identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,37 @@ const GOAL_BINDING_MATCH_SCHEMA_VERSION = "loopx_goal_binding_match_v1";
const GOAL_ID = /^[A-Za-z0-9._:-]{1,200}$/;
const GOAL_INSTANCE_ID = /^ginst_[0-9a-f]{32}$/;

type GoalId = Readonly<{
export type GoalId = Readonly<{
kind: "goal_id";
value: string;
}>;

type GoalInstanceId = Readonly<{
export type GoalInstanceId = Readonly<{
kind: "goal_instance_id";
value: string;
}>;

export type ExactGoalRef = Readonly<{
kind: "goal_ref";
goalId: GoalId;
goalInstanceId: GoalInstanceId;
}>;

type GoalRef =
| Readonly<{
kind: "legacy_goal_ref";
goalId: GoalId;
}>
| ExactGoalRef;

export type ExactGoalRefParseResult =
| Readonly<{ kind: "parsed"; value: ExactGoalRef }>
| Readonly<{
kind: "goal_ref";
goalId: GoalId;
goalInstanceId: GoalInstanceId;
kind: "invalid";
issue:
| "invalid_goal_id"
| "missing_goal_instance_id"
| "invalid_goal_instance_id";
}>;

type BindingOwner = "source_registry" | "global_projection";
Expand All @@ -42,10 +54,22 @@ type IdentityIssue =
reason: UnavailableReason;
}>;

type GoalRefIssue = Extract<
IdentityIssue,
Readonly<{
kind: "invalid_goal_id" | "invalid_goal_instance_id";
side: IdentitySide;
}>
>;

type Parsed<Value> =
| Readonly<{ kind: "parsed"; value: Value }>
| Readonly<{ kind: "invalid"; issue: IdentityIssue }>;

type ParsedGoalRef =
| Readonly<{ kind: "parsed"; value: GoalRef }>
| Readonly<{ kind: "invalid"; issue: GoalRefIssue }>;

type Authority =
| Readonly<{ kind: "present"; goal: GoalRef }>
| Readonly<{ kind: "absent" }>
Expand All @@ -59,7 +83,7 @@ function parseBindingOwner(value: unknown): Parsed<BindingOwner> {
return { kind: "invalid", issue: { kind: "invalid_binding_owner" } };
}

function parseGoalRef(value: unknown, side: IdentitySide): Parsed<GoalRef> {
function parseGoalRef(value: unknown, side: IdentitySide): ParsedGoalRef {
const raw = jsonObject(value);
if (!raw || typeof raw.goal_id !== "string" || !GOAL_ID.test(raw.goal_id)) {
return {
Expand Down Expand Up @@ -93,6 +117,17 @@ function parseGoalRef(value: unknown, side: IdentitySide): Parsed<GoalRef> {
};
}

export function parseExactGoalRef(value: unknown): ExactGoalRefParseResult {
const parsed = parseGoalRef(value, "binding");
if (parsed.kind === "invalid") {
return { kind: "invalid", issue: parsed.issue.kind };
}
if (parsed.value.kind === "legacy_goal_ref") {
return { kind: "invalid", issue: "missing_goal_instance_id" };
}
return { kind: "parsed", value: parsed.value };
}

function parseAuthority(value: unknown): Parsed<Authority> {
const raw = jsonObject(value);
if (!raw) {
Expand Down
Loading
Loading