diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
index 0756239373..005a9afcad 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
+++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts
@@ -12,7 +12,7 @@ type FieldCopy = Record> = {
en: {
- goal_storage: { displayName: "New Goal storage target", description: "Fixed at creation and used after reviewed promotion. Existing Goals require a separate backed-up migration." },
+ goal_storage: { displayName: "New Goal authority", description: "Opt in to canonical creation and choose the storage and execution policy for future Goals. Existing Goals require a separate backed-up migration." },
manager_runtime: {
displayName: "Runtime",
description: "Selects the persistent host-tool profile used by owner manager conversations.",
@@ -78,7 +78,7 @@ const capabilityCopy: Record> = {
},
},
"zh-CN": {
- goal_storage: { displayName: "新 Goal 的目标存储", description: "创建时固定,审核晋升后生效。已有 Goal 需要单独备份、迁移;更改这里不会迁移数据。" },
+ goal_storage: { displayName: "新 Goal 的权威存储", description: "可启用 canonical 创建,选择之后新 Goal 的存储与执行策略。已有 Goal 仍需单独备份、迁移。" },
manager_runtime: {
displayName: "运行环境",
description: "选择管家会话持续生效的宿主工具模式。",
@@ -147,7 +147,9 @@ const capabilityCopy: Record> = {
const fieldCopy: Record = {
en: {
- new_goal_provider: { label: "New Goal storage target (after promotion)", description: "File or SQLite; this setting does not perform promotion or migration." },
+ new_goal_provider: { label: "New Goal storage provider", description: "File or SQLite; frozen at creation, independently of execution policy." },
+ canonical_creation: { label: "Create canonical authority", description: "Future Goals only. Disabled retains the post-promotion target; failures require the original creation retry." },
+ new_goal_handoff_mode: { label: "New Goal execution policy", description: "soft_claim or hard_lease, used with canonical creation. Agents inherit the Goal policy; tool authority is unchanged." },
runtime_profile: { label: "Runtime profile", description: "Restricted keeps scoped LoopX reads only. Trusted owner enables normal host tools while protected operations retain separate checks." },
selection_policy: { label: "Selection policy", description: "Preferred allows an explicit user choice; pinned rejects another executor; flexible permits fallback only inside the eligible pool." },
executor_endpoint: { label: "Primary steward executor", description: "The preferred or pinned executor for this machine. In a flexible pool it is tried first when available." },
@@ -174,7 +176,9 @@ const fieldCopy: Record = {
enabled_agents: { label: "Enabled Goal Agents", description: "Enter one registered Goal-local Agent id per line. A private binding currently accepts exactly one Agent." },
},
"zh-CN": {
- new_goal_provider: { label: "新 Goal 的目标存储(晋升后生效)", description: "选择 File 或 SQLite;保存设置不会自动晋升,也不会迁移已有 Goal。" },
+ new_goal_provider: { label: "新 Goal 的存储 provider", description: "选择 File 或 SQLite;创建时固定,与执行策略分别选择。" },
+ canonical_creation: { label: "建立 canonical 权威存储", description: "仅影响此后新建的 Goal。关闭时仅固定晋升后的目标;失败须重试原创建操作。" },
+ new_goal_handoff_mode: { label: "新 Goal 的执行策略", description: "canonical 创建使用 soft_claim 或 hard_lease。Agent 继承 Goal 策略;不授予工具权限。" },
runtime_profile: { label: "运行模式", description: "restricted 仅使用受限 LoopX 读取;trusted_owner 开放常规宿主工具,但受保护操作仍单独校验。" },
selection_policy: { label: "选择策略", description: "preferred 允许用户显式改选;pinned 拒绝其他执行器;flexible 只在已授权资源池内回退。" },
executor_endpoint: { label: "首选管家执行器", description: "本机首选或锁定的执行器;灵活池模式下优先尝试它。" },
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
index 7a2f06b44b..a42c44b650 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
@@ -54,8 +54,8 @@ function completeMachineConfiguration(
// The guided steward editor owns the v1 selection-policy fields. Opening an
// installed v0 preference in that form is an explicit migration preview;
// JSON mode can still submit the legacy shape unchanged when needed.
- if (capability.capability_id === "steward_executor"
- && (Object.hasOwn(draft, "selection_policy") || Object.hasOwn(draft, "eligible_endpoints"))) {
+ if (capability.capability_id === "goal_storage" || (capability.capability_id === "steward_executor"
+ && (Object.hasOwn(draft, "selection_policy") || Object.hasOwn(draft, "eligible_endpoints")))) {
complete.schema_version = configurationObject(capability.default).schema_version;
}
return complete;
@@ -373,8 +373,8 @@ export function MachineConfigurationSettings({ section, onChanged }: { section:
{locale === "zh-CN" ? "仅影响此后创建的 Goal" : "Future Goals only"}{locale === "zh-CN"
- ? "创建时固定选择,审核晋升后生效。已有 Goal 不变;迁移需单独备份、停止写入并结算租约。"
- : "Fixed at creation and used after reviewed promotion. Existing Goals are unchanged; migration requires a separate backup, stopped writers and settled leases."}
+ ? "启用 canonical 创建后,新 Goal 直接建立权威存储,并采用所选执行策略;失败时须重试原创建操作。关闭时仅固定晋升后的目标存储。已有 Goal 的升级仍需备份、停止写入和结算租约;这里不授予工具权限。"
+ : "With canonical creation enabled, new Goals establish authority with the selected execution policy; retry the original operation after failure. Disabled only freezes the post-promotion target. Existing Goals still require backup, stopped writers and settled leases for upgrade; this setting grants no tool permissions."}
) : null}
diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md
index f6efd1a772..62efaed604 100644
--- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md
+++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md
@@ -41,6 +41,18 @@ Frozen failures/missing evidence remain visible. T4 deletes proven redundant
owners alongside implementation, without waiting for R6 or all Python to vanish.
This replaces stale current-count estimates, not historical execution evidence.
+**Fresh creation opt-in (2026-10-04, proposed).** The existing
+[device setting and CLI/App creation owner](../../reference/local-authority-provider-selection.md#new-goal-authority-machine-setting)
+can freeze a File/SQLite target and `soft_claim`/`hard_lease` policy, initialize
+empty canonical authority and recover the original creation receipt. Isolated
+real-provider CLI/HTTP and packaged settings checks cover original-operation
+retry, writer fencing, lost completed authority and rejected policy recovery.
+Completed retries no longer parse the Python Markdown source; unfinished fresh
+creation still captures it only when the typed owner requests it. Default-off
+and released v0 behavior remain; live legacy writers retain callers. This is a
+bounded L9 prerequisite, not installed upgrade, whole-Goal recovery, D2, cohort
+admission or release-default acceptance. Those existing exits remain open.
+
File retained-state storage now reuses the existing TS checkpoint/delta codec,
stacked on #5063's verified read cache and RPC budgets. Original revisions,
receipts and full historical projections survive the physical format upgrade.
diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md
index 4d5eba9aee..485b125321 100644
--- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md
+++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md
@@ -34,6 +34,15 @@
D2 已通过;冻结的失败/缺项保持可见。T4 随实现删除已证明重复的 owner,不等 R6
或所有 Python 消失。本节替代陈旧的当前数量估算,不覆盖历史执行证据。
+**新建 opt-in(2026-10-04,拟议)。** 既有
+[设备设置和 CLI/App 创建 owner](../../reference/local-authority-provider-selection.md#new-goal-authority-machine-setting)
+可固定 File/SQLite 目标与 `soft_claim`/`hard_lease` 策略,初始化空 canonical
+权威并恢复原创建回执。隔离真实 provider 的 CLI/HTTP 与打包设置验证覆盖原操作
+重试、旧 writer fence、已完成存储丢失和非法策略恢复。完成后的重试不再解析
+Python Markdown 源;未完成的新建仅在 typed owner 请求时捕获源。默认关闭及
+发布版 v0 行为保持,活跃 legacy writer 仍有调用方。这是有界 L9 前置,不代表
+安装态升级、整 Goal 恢复、D2、cohort 准入或发布默认验收;这些既有出口保持开放。
+
**所有权精简阶段(2026-10-01)。** R5/T4 将存储晋升与策略迁移分开:新 CLI
promote 默认保留策略,正常策略目标收敛为 soft/hard。canonical 策略迁移复用晋升
规则、完整归档和 command receipt owner,用一笔 CAS 保留 assignment、lease
diff --git a/docs/reference/images/new-goal-authority/after.png b/docs/reference/images/new-goal-authority/after.png
new file mode 100644
index 0000000000..e41b23124b
Binary files /dev/null and b/docs/reference/images/new-goal-authority/after.png differ
diff --git a/docs/reference/images/new-goal-authority/before.png b/docs/reference/images/new-goal-authority/before.png
new file mode 100644
index 0000000000..723ea55513
Binary files /dev/null and b/docs/reference/images/new-goal-authority/before.png differ
diff --git a/docs/reference/images/new-goal-authority/invalid-policy.png b/docs/reference/images/new-goal-authority/invalid-policy.png
new file mode 100644
index 0000000000..205937536a
Binary files /dev/null and b/docs/reference/images/new-goal-authority/invalid-policy.png differ
diff --git a/docs/reference/images/new-goal-authority/mobile.png b/docs/reference/images/new-goal-authority/mobile.png
new file mode 100644
index 0000000000..f11bbcfdf8
Binary files /dev/null and b/docs/reference/images/new-goal-authority/mobile.png differ
diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md
index ae2ab6d2b2..2fb8bd79a4 100644
--- a/docs/reference/local-authority-provider-selection.md
+++ b/docs/reference/local-authority-provider-selection.md
@@ -59,64 +59,132 @@ contract; PostgreSQL's real-server qualification remains a separate gate.
See [reviewed promotion and recovery](reviewed-coordination-promotion.md) for the explicit saved-plan CLI journey.
-## New Goal storage target (machine setting)
-
-The **New Goal storage target** setting fixes a File or SQLite target at
-creation. It is not live inheritance, automatic promotion, or an existing-Goal
-migration. Until separately reviewed promotion, the existing legacy source is
-still authoritative. After promotion the selected provider serves canonical
-Todo/lease state; Run artifacts and other independently owned stores are not
-moved by this preference.
+## New Goal authority (machine setting)
+
+**Settings → Capability Center → Device defaults → New Goal authority** selects
+File or SQLite independently of the execution policy. Enable **Create canonical
+authority** to initialize future empty Goals directly with `soft_claim` or
+`hard_lease`. Agents inherit the Goal policy; this grants no tool, repository,
+scheduler, account or network permission and does not migrate existing data.
+
+Canonical creation is default-off. An absent namespace, the released v0 shape,
+or v1 with `canonical_creation=false` retains the post-promotion target behavior.
+Opening v0 in the guided editor previews a v1 envelope upgrade with creation
+still disabled. The CLI continues to accept v0.
+
+Save this namespace document as `goal-storage.json`:
+
+```json
+{
+ "schema_version": "loopx_goal_storage_defaults_v1",
+ "new_goal_provider": "sqlite",
+ "canonical_creation": true,
+ "new_goal_handoff_mode": "hard_lease"
+}
+```
-Use **Settings → Capability Center → Device defaults → New Goal storage target**
-or the revision-checked CLI:
+Preview, apply the exact reviewed revision, then inspect and create:
```sh
-# goal-storage.json:
-# {"schema_version":"loopx_goal_storage_defaults_v0","new_goal_provider":"sqlite"}
loopx machine-config preview --namespace goal_storage --config-json goal-storage.json
loopx machine-config apply --namespace goal_storage --config-json goal-storage.json \
--expected-plan-revision PLAN_REVISION --execute
loopx machine-config inspect
loopx bootstrap --project ./new-project --goal-id new-project --dry-run
+loopx bootstrap --project ./new-project --goal-id new-project
+loopx todo list --goal-id new-project
+```
+
+Creation reports `storage_selection.authority_initialized=true`, its original
+operation and provider receipt, and `legacy_writer_fenced=true`. Complete Todo
+reads report canonical `source_authority` and `legacy_fallback_used=false`.
+Saving a preference or publishing a registry entry alone is not successful
+creation. Run artifacts and independently owned stores do not move.
+
+CLI and App reuse one typed creation owner. The registry atomically freezes the
+original operation and target before initialization. The owner verifies the
+registered source, complete empty Todo/lease inventory and current bytes under
+the existing writer locks. It engages a creation fence, commits the native
+projection and original receipt, and durably records completion before success.
+Fresh creation has no shadow qualification and never fabricates capture events.
+Nonempty or captured sources require reviewed migration.
+
+After interruption, rerun the same CLI bootstrap, or use **Retry original
+operation** on the App card. Changed device defaults cannot retarget that
+operation. Recovery must match its operation and workspace; a competing creator
+cannot adopt it. The original receipt survives later native writes, so replay
+cannot erase Todos or repeat their creation. An unavailable selected provider
+fails visibly without Markdown fallback. Lost completed authority requires full
+backup recovery and cannot be treated as empty creation. Generic forced
+bootstrap cannot rebuild an opted-in Goal.
+
+
+Settings and recovery views / 设置与恢复界面
+
+Synthetic workspace data; the settings use a real isolated backend. The first
+view is the released v0 editor; the remaining views show the proposed v1 path.
+
+Before: the provider setting only chooses the post-promotion target.
+
+
+
+After: provider, explicit canonical opt-in and execution policy, with applied
+configuration readback.
+
+
+
+An unsupported `legacy` policy is rejected before apply; the previous valid
+configuration remains. Correcting the policy allows preview and apply again.
+
+
+
+The same device settings at a narrow viewport:
+
+
+
+
+
+To disable future canonical creation, preview and apply the same v1 document
+with `canonical_creation=false`. To remove the whole preference:
+
+```sh
+loopx machine-config remove --namespace goal_storage
+loopx machine-config remove --namespace goal_storage \
+ --expected-plan-revision PLAN_REVISION --execute
+loopx machine-config inspect
```
-The preview reports `storage_target`; creation reports `storage_selection` with
-`promotion_performed=false`. CLI and App creation share the same bootstrap
-owner. Creation stores its intent before provider initialization, so retry after
-interruption uses the same target even if the machine preference changed.
-If App creation fails during initialization, use **Retry original operation**
-on that creation card. It resumes the recorded target before adding initial
-Todos or starting a Turn. A persistent initialization failure remains an error;
-the presence of a registry entry alone is not successful creation. Recovery must
-match the original App operation and its validated workspace. Registration
-records `creation_operation_id` atomically with the Goal; a competing creation
-of the same id, even in the same workspace, is rejected before initialization
-or initial Todos. The create-only check is repeated under the registry lock.
-Older incomplete cards without this binding require inspection of the existing
-Goal and its canonical bootstrap/recovery path; they cannot adopt it by id.
-Already-applied cards continue to return their original receipt.
-Reconnecting an existing Goal, including an implicit File Goal, does not adopt
-a newer machine default. Importing existing Markdown does not count as a new
-empty Goal. Explicit provider selection never falls back on failure.
-
-Without this namespace, existing behavior remains unchanged. To stop applying
-the preference to future Goals, preview `loopx machine-config remove
---namespace goal_storage`, then use its returned plan revision with `--execute`.
-Configuration rollback also affects future creation only. Neither operation
-switches existing storage or removes data. A File target keeps implicit File
-routing until a committed authority exists; it does not create a dangling
-identity-bound selector for an empty File document.
-
-For already-promoted Goals use the [reviewed File/SQLite cutover](file-authority-state-log.md#reviewed-filesqlite-cutover):
-stop writers, settle leases, review the saved plan, retain verified backups,
-then migrate. Reverse migration must preserve newer writes. New-Goal defaults
-and current-provider selection are separate facts. This opt-in setting does
-not change the release default or complete D2/D3 qualification.
-
-### 新 Goal 的目标存储
-
-这是创建时固定的目标,审核晋升后才接管 canonical Todo/lease;不是“所有数据
-已经存入 SQLite”。更改默认值只影响此后创建的空 Goal,既有 Goal、重新连接或
-导入已有 Markdown 均不自动切换。创建中断后重试沿用已记录的选择。关闭或回滚
-设置不迁回数据;已有 Goal 需停止写入、结算租约,走独立的备份和审核迁移流程。
+Use the removal preview's revision. Rollback also affects future creation only;
+neither operation switches existing storage, removes its fence or reopens its
+old writer. Reconnection and import keep their recorded route. Existing Markdown
+Goals use [reviewed promotion and recovery](reviewed-coordination-promotion.md);
+already-canonical Goals use the [reviewed File/SQLite cutover](file-authority-state-log.md#reviewed-filesqlite-cutover).
+Retain verified backups, stop writers, settle leases and preserve newer writes
+on reverse migration. Supported historical backup/format/receipt readers remain.
+
+This opt-in path does not close full existing-Goal upgrade, D2 sustained
+qualification or the release-default decision. Trial admission and release
+default admission remain separate; the existing RFC acceptance is unchanged.
+
+### 新 Goal 的权威存储
+
+在“设置 → 能力中心 → 此设备默认 → 新 Goal 的权威存储”中,分别选择 File/SQLite
+和 `soft_claim`/`hard_lease`,并显式启用 canonical 创建。默认关闭;旧 v0 或关闭
+状态仍只固定晋升后的目标。表单以关闭状态预览 v1 升级,CLI 继续接受旧格式。
+Agent 继承 Goal 策略;此设置不授予工具、仓库、账户或网络权限。
+
+CLI 使用上面的完整 JSON 和 preview/apply/inspect/bootstrap 命令;App 用现有
+表单预览、应用并读回。成功须含 `authority_initialized=true`、原创建回执和已
+读回的写入 fence;Todo list 须显示 canonical provider。保存偏好或出现 registry
+记录本身不算成功,也不代表 Run 等独立存储已经迁移。
+
+失败时重新执行原 bootstrap,或在 App 创建卡上“重试原操作”。目标和身份已固定,
+后续默认值不能改写它。旧写入先被 fence;原生提交和回执核对后才持久记录完成。
+已有 Todo、租约历史或 capture 的源须走独立审核迁移,不伪造 shadow 资格。完成
+后的存储丢失须恢复完整备份,不能重新创建空库;通用 force 不能重建。原回执在
+后续写入后仍可读回,重试不得丢失或重复 Todo。
+
+关闭或按上面的 remove 预览/执行命令删除偏好,只影响之后新建;不会迁回已有数据、
+删除 fence 或重新开放旧 writer。既有 Goal 升级仍需备份、停止写入、结算租约和
+审核计划;反向迁移须保留新增写入。受支持的旧备份、格式和原回执恢复能力保留。
+此路径不代表完整升级、D2 长期资格或发布默认已通过。
diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py
index 628fea0a52..c375d36891 100644
--- a/loopx/bootstrap.py
+++ b/loopx/bootstrap.py
@@ -1,11 +1,13 @@
from __future__ import annotations
import re
+from uuid import uuid4
from pathlib import Path
from .capabilities.machine_configuration.goal_storage import new_goal_storage_target, initialize_goal_storage_target
from .registry import find_registry_goal
from .control_plane.coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed
+from .control_plane.coordination.legacy_writer_fence import require_registry_source_write_allowed
from .control_plane.coordination.runtime_shadow_writer_adapter import require_runtime_shadow_capture_prepared, begin_todo_runtime_shadow_capture, settle_todo_runtime_shadow_capture
from .control_plane.projects.registry_codec import (
load_project_registry,
@@ -390,15 +392,22 @@ def bootstrap_project(
if creation_operation_id is not None:
goal_entry["creation_operation_id"] = creation_operation_id
previous_goal = find_registry_goal(registry, goal_id)
+ if creation_operation_id is not None and previous_goal is not None:
+ raise GoalCreationConflictError("Goal id was registered by another operation")
storage_target = ((previous_goal or {}).get("coordination") or {}).get("storage_target")
if previous_goal is None and not state_file.exists():
storage_target = new_goal_storage_target(runtime_root)
if storage_target is not None:
goal_entry.setdefault("coordination", {})["storage_target"] = storage_target
+ canonical_creation = (storage_target or {}).get("schema_version") == "loopx_new_goal_storage_target_v1"
+ if canonical_creation and previous_goal is not None and force:
+ raise ValueError("Canonical creation cannot rebuild existing state; restore or migrate through its owning operation")
+ if canonical_creation:
+ goal_entry["creation_operation_id"] = creation_operation_id or (previous_goal or {}).get("creation_operation_id") or f"goal-create:{uuid4().hex}"
registry, registry_goal_action = merge_goal(registry, goal_entry, force=force)
state_exists = state_file.exists()
- state_action = "created"
+ state_action = "kept-existing" if canonical_creation and previous_goal is not None else "created"
if state_exists and force and preserve_todos:
state_action = "kept-existing-preserve-todos"
elif state_exists and not force:
@@ -408,7 +417,7 @@ def bootstrap_project(
repaired_state_text: str | None = None
repaired_todo_source_roles: list[str] = []
- if state_exists and state_action in {
+ if not canonical_creation and state_exists and state_action in {
"kept-existing",
"kept-existing-preserve-todos",
}:
@@ -433,7 +442,7 @@ def bootstrap_project(
}
force_bootstrap_warning = None
declared_handoff_mode = HANDOFF_MODE_LEGACY
- if state_exists and force:
+ if not canonical_creation and state_exists and force:
# A forced rebuild replaces todos, never the goal's handoff contract:
# the declared mode is carried into the rewritten front matter, and an
# invalid declaration fails closed before anything is rewritten.
@@ -524,12 +533,20 @@ def bootstrap_project(
shadow_capture = None
shadow_evidence: dict[str, Any] = {}
if not dry_run:
+ # Reconnect enters the same typed receipt owner before compatibility
+ # reads or rebuild checks. It alone decides whether unfinished creation
+ # needs a full source capture; completed authority never needs Markdown.
+ if canonical_creation and previous_goal is not None:
+ storage_selection = initialize_goal_storage_target(runtime_root,
+ {**previous_goal, "state_file": str(state_file)}, registry_path=registry_path)
+ canonical_reconnect = storage_selection is not None and storage_selection.get("authority_initialized") is True
+ canonical_bootstrap_transport = canonical_creation
with project_registry_transaction(
registry_path,
operation="bootstrap_registry",
create=dict,
) as registry_transaction, legacy_todo_write_transaction(
- registry_path, goal_id, state_file, None, "bootstrap_state", False,
+ registry_path, goal_id, state_file, None, "bootstrap_state", canonical_creation,
runtime_root=runtime_root,
):
current_registry = registry_transaction.payload_copy()
@@ -544,6 +561,10 @@ def bootstrap_project(
goal_entry.setdefault("coordination", {}).pop("storage_target", None)
if frozen is not None:
goal_entry["coordination"]["storage_target"] = frozen
+ frozen_target = goal_entry.get("coordination", {}).get("storage_target") or {}
+ canonical_creation = frozen_target.get("schema_version") == "loopx_new_goal_storage_target_v1"
+ if canonical_creation and force and (current_goal is not None or state_file.exists()):
+ raise ValueError("Canonical creation cannot rebuild existing state; restore or migrate through its owning operation")
# A first explicit bootstrap has no previous Goal authority to fence.
# Existing Goals still resolve and authorize their original route.
@@ -554,14 +575,20 @@ def bootstrap_project(
if isinstance(previous_goal, dict) and previous_goal.get("id"):
require_legacy_state_replacement_allowed(runtime_root=previous_root,
goal_id=str(previous_goal["id"]), goal=previous_goal)
- original = state_file.read_text(encoding="utf-8") if state_file.exists() else ""
- if force or not state_file.exists():
+ original = state_file.read_text(encoding="utf-8") if not canonical_reconnect and state_file.exists() else ""
+ if not canonical_reconnect and (force or not state_file.exists()):
require_legacy_state_replacement_allowed(runtime_root=runtime_root,
goal_id=goal_id, goal=current_goal)
- state_action = ("kept-existing-preserve-todos" if force and preserve_todos else "kept-existing") if state_file.exists() and (not force or preserve_todos) else "replaced" if state_file.exists() else "created"
+ if canonical_reconnect:
+ state_action = "kept-existing"
+ else:
+ state_action = ("kept-existing-preserve-todos" if force and preserve_todos else "kept-existing") if state_file.exists() and (not force or preserve_todos) else "replaced" if state_file.exists() else "created"
+ for action in actions:
+ if action.get("path") == str(state_file):
+ action["action"] = state_action
planned = original
if state_action in {"created", "replaced"}:
- declared_handoff_mode = goal_handoff_mode(original) if original and force else HANDOFF_MODE_LEGACY
+ declared_handoff_mode = goal_handoff_mode(original) if original and force else frozen_target.get("handoff_mode", HANDOFF_MODE_LEGACY)
planned = render_state_markdown(
project=project,
goal_id=goal_id,
@@ -572,16 +599,21 @@ def bootstrap_project(
execution_profile=execution_profile,
handoff_mode=declared_handoff_mode,
)
- else:
+ elif not canonical_creation:
planned, repaired_todo_source_roles = repair_missing_todo_source_sections(original)
if planned != original:
- shadow_capture = begin_todo_runtime_shadow_capture(registry_path=registry_path,
- runtime_root=runtime_root, goal_id=goal_id, state_path=state_file,
- write_class="bootstrap_state", original_text=original)
- shadow_capture.prepare(planned)
- require_runtime_shadow_capture_prepared(shadow_capture, runtime_root=runtime_root, goal_id=goal_id)
+ if canonical_bootstrap_transport:
+ require_registry_source_write_allowed(registry_path=registry_path, runtime_root=runtime_root,
+ goal_id=goal_id, state_file=state_file)
+ if not canonical_creation:
+ shadow_capture = begin_todo_runtime_shadow_capture(registry_path=registry_path,
+ runtime_root=runtime_root, goal_id=goal_id, state_path=state_file,
+ write_class="bootstrap_state", original_text=original)
+ shadow_capture.prepare(planned)
+ require_runtime_shadow_capture_prepared(shadow_capture, runtime_root=runtime_root, goal_id=goal_id)
atomic_write_state_text(state_file, planned)
- shadow_capture.committed()
+ if shadow_capture is not None:
+ shadow_capture.committed()
if todo_source_migration is not None:
todo_source_migration["applied"] = True
current_registry.setdefault("schema_version", "0.1")
@@ -592,7 +624,7 @@ def bootstrap_project(
# Registry intent survives an interrupted initialization. Reconnect retries
# it outside the legacy/registry locks; changing machine defaults cannot
# retarget that Goal. The TS owner refuses replacing an existing provider.
- storage_selection = initialize_goal_storage_target(runtime_root, find_registry_goal(registry, goal_id) or {})
+ storage_selection = initialize_goal_storage_target(runtime_root, find_registry_goal(registry, goal_id) or {}, registry_path=registry_path)
if shadow_capture is not None:
shadow_evidence = settle_todo_runtime_shadow_capture({}, registry_path=registry_path,
runtime_root=runtime_root, goal_id=goal_id, capture=shadow_capture, emit_disabled=False)
diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py
index da94e8e318..0d697d5f32 100644
--- a/loopx/capabilities/configuration_ui.py
+++ b/loopx/capabilities/configuration_ui.py
@@ -94,9 +94,14 @@ def capability_configuration_editor(
},
"goal_storage": {
"supported_scopes": ["machine"], "writable_scopes": ["machine"],
- "fields": [_field("new_goal_provider", "New Goal storage target (after promotion)", "select",
+ "fields": [_field("new_goal_provider", "New Goal storage provider", "select",
options=["file", "sqlite"], required=True,
- description="Fixed at creation. Existing Goals need a separate backed-up migration; this setting does not promote them.")],
+ description="Fixed at creation. Existing Goals need a separate backed-up migration."),
+ _field("canonical_creation", "Create canonical authority", "boolean", required=True,
+ description="Explicit opt-in for future Goals. Disabled retains the post-promotion target behavior."),
+ _field("new_goal_handoff_mode", "New Goal execution policy", "select",
+ options=["soft_claim", "hard_lease"], required=True,
+ description="Used only with canonical creation. Agents inherit the Goal policy; this grants no tool permissions.")],
},
"todo_replan_cadence": {
"supported_scopes": ["machine", "goal"],
diff --git a/loopx/capabilities/machine_configuration/goal_storage.py b/loopx/capabilities/machine_configuration/goal_storage.py
index 8d3629fba5..28ca5a7ad3 100644
--- a/loopx/capabilities/machine_configuration/goal_storage.py
+++ b/loopx/capabilities/machine_configuration/goal_storage.py
@@ -9,27 +9,33 @@
from ...control_plane.effect_runtime import effect_runtime_result
GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v0"
+CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v1"
NEW_GOAL_STORAGE_METHOD = "coordination.local_authority.new_goal_storage"
def normalize_goal_storage_defaults(raw: Mapping[str, Any]) -> dict[str, Any]:
# Configuration-envelope validation only; target resolution/admission is TS-owned.
- if set(raw) != {"schema_version", "new_goal_provider"} or raw.get("schema_version") != GOAL_STORAGE_DEFAULTS_SCHEMA:
+ canonical = raw.get("schema_version") == CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA
+ fields = {"schema_version", "new_goal_provider", "canonical_creation", "new_goal_handoff_mode"} if canonical else {"schema_version", "new_goal_provider"}
+ if set(raw) != fields or (not canonical and raw.get("schema_version") != GOAL_STORAGE_DEFAULTS_SCHEMA):
raise ValueError("goal_storage requires schema_version and new_goal_provider")
if raw.get("new_goal_provider") not in ("file", "sqlite"):
raise ValueError("new_goal_provider must be file or sqlite")
+ if canonical and (type(raw["canonical_creation"]) is not bool or raw["new_goal_handoff_mode"] not in ("soft_claim", "hard_lease")):
+ raise ValueError("canonical_creation must be boolean; new_goal_handoff_mode must be soft_claim or hard_lease")
return dict(raw)
def goal_storage_machine_configuration_namespace() -> MachineConfigurationNamespace:
return MachineConfigurationNamespace(
- namespace="goal_storage", schema_versions=frozenset({GOAL_STORAGE_DEFAULTS_SCHEMA}),
+ namespace="goal_storage", schema_versions=frozenset({GOAL_STORAGE_DEFAULTS_SCHEMA, CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA}),
normalize=normalize_goal_storage_defaults, project_public=dict,
apply_public_update=lambda _current, update: dict(update),
- title="New Goal storage target",
- description=("Fixed when a new Goal is created; used after reviewed promotion. "
- "Does not promote Goals or migrate existing data. Existing Goals keep their selection."),
- default_configuration={"schema_version": GOAL_STORAGE_DEFAULTS_SCHEMA, "new_goal_provider": "file"},
+ title="New Goal authority",
+ description=("Opt in to canonical creation and choose its execution policy. "
+ "Fixed for future Goals only; existing data requires a separate reviewed migration."),
+ default_configuration={"schema_version": CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA, "new_goal_provider": "file",
+ "canonical_creation": False, "new_goal_handoff_mode": "hard_lease"},
documentation={"path": "docs/reference/local-authority-provider-selection.md",
"url": "https://github.com/loopx-project/loopx/blob/main/docs/reference/local-authority-provider-selection.md"},
)
@@ -45,9 +51,29 @@ def new_goal_storage_target(runtime_root: Path) -> dict[str, Any] | None:
return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, {"action": "resolve", "configuration": raw})
-def initialize_goal_storage_target(runtime_root: Path, goal: Mapping[str, Any]) -> dict[str, Any] | None:
+def initialize_goal_storage_target(runtime_root: Path, goal: Mapping[str, Any], *, registry_path: Path | None = None) -> dict[str, Any] | None:
target = (goal.get("coordination") or {}).get("storage_target")
if target is None:
return None
- return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, {"action": "initialize", "runtime_root": str(runtime_root),
- "goal_id": goal["id"], "target": target})
+ request = {"action": "initialize", "runtime_root": str(runtime_root), "goal_id": goal["id"], "target": target}
+ if target.get("schema_version") == "loopx_new_goal_storage_target_v1":
+ from ...registry import resolve_state_file
+ from ...control_plane.coordination.authority_source_capture import authority_registry_source
+ from ...agent_registry import registered_agent_ids_for_goal
+ if registry_path is None:
+ raise ValueError("Canonical creation requires its registered source")
+ state_path = resolve_state_file(Path(goal["repo"]), goal["state_file"])
+ with authority_registry_source(registry_path) as witness:
+ request.update(creation_operation_id=goal.get("creation_operation_id"), source_snapshot={
+ "state_path": str(state_path.resolve()),
+ "registry_source": {**witness, "registered_agents": registered_agent_ids_for_goal(dict(goal))}})
+ result = effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request)
+ # Native receipt readback needs no Markdown parsing. Only the typed
+ # owner can request a complete source capture for unfinished creation.
+ if result.get("source_capture_required") is not True:
+ return result
+ from ...control_plane.coordination.runtime_shadow import build_runtime_shadow_source_snapshot
+ projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime_root,
+ state_path=state_path, registry_path=registry_path)
+ request.update(creation_operation_id=goal.get("creation_operation_id"), projection=projection, source_snapshot=snapshot)
+ return effect_runtime_result(NEW_GOAL_STORAGE_METHOD, request)
diff --git a/loopx/chat_actions.py b/loopx/chat_actions.py
index 1e85e8cdd2..caa02ad3c9 100644
--- a/loopx/chat_actions.py
+++ b/loopx/chat_actions.py
@@ -602,7 +602,7 @@ def _apply_goal_create(
# Registry publication precedes storage initialization. Resume the
# frozen target through its TS owner before any downstream effects;
# re-running Markdown bootstrap could overwrite a promoted Goal.
- initialize_goal_storage_target(runtime_root, existing_goal)
+ initialize_goal_storage_target(runtime_root, existing_goal, registry_path=self.registry_path)
result = {"ok": True}
else:
try:
diff --git a/loopx/control_plane/coordination/coordination_state_contract.generated.ts b/loopx/control_plane/coordination/coordination_state_contract.generated.ts
index 33bef8bdbe..79c81a5ee8 100644
--- a/loopx/control_plane/coordination/coordination_state_contract.generated.ts
+++ b/loopx/control_plane/coordination/coordination_state_contract.generated.ts
@@ -66,6 +66,7 @@ export const SHADOW_OUTBOX_MANIFEST_SCHEMA = "loopx_shadow_outbox_manifest_v1";
export const SHADOW_EXACT_OUTBOX_MANIFEST_SCHEMA = "loopx_shadow_outbox_manifest_v2";
export const LEGACY_COORDINATION_WRITER_FENCE_SCHEMA = "loopx_legacy_coordination_writer_fence_v0";
+export const NEW_GOAL_WRITER_FENCE_SCHEMA = "loopx_new_goal_writer_fence_v0";
export const LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA = "loopx_legacy_coordination_writer_fence_engage_request_v0";
export const LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA = "loopx_legacy_coordination_writer_fence_result_v0";
export const LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA = "loopx_legacy_coordination_write_check_request_v0";
@@ -305,6 +306,7 @@ export const COORDINATION_STATE_CONTRACT = deepFreeze({
},
"legacy_writer_fence_protocol": {
"fence_schema": LEGACY_COORDINATION_WRITER_FENCE_SCHEMA,
+ "creation_fence_schema": NEW_GOAL_WRITER_FENCE_SCHEMA,
"engage_request_schema": LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA,
"result_schema": LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA,
"write_check_request_schema": LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA,
diff --git a/loopx/control_plane/coordination/coordination_state_contract_generated.py b/loopx/control_plane/coordination/coordination_state_contract_generated.py
index f939d2483b..8fb413c7c8 100644
--- a/loopx/control_plane/coordination/coordination_state_contract_generated.py
+++ b/loopx/control_plane/coordination/coordination_state_contract_generated.py
@@ -161,6 +161,7 @@ def _freeze(value: Any) -> Any:
'outbox_manifest_schema': 'loopx_shadow_outbox_manifest_v1',
'exact_outbox_manifest_schema': 'loopx_shadow_outbox_manifest_v2'},
'legacy_writer_fence_protocol': {'fence_schema': 'loopx_legacy_coordination_writer_fence_v0',
+ 'creation_fence_schema': 'loopx_new_goal_writer_fence_v0',
'engage_request_schema': 'loopx_legacy_coordination_writer_fence_engage_request_v0',
'result_schema': 'loopx_legacy_coordination_writer_fence_result_v0',
'write_check_request_schema': 'loopx_legacy_coordination_write_check_request_v0',
@@ -271,6 +272,7 @@ def _freeze(value: Any) -> Any:
SHADOW_EXACT_OUTBOX_MANIFEST_SCHEMA: Final[str] = 'loopx_shadow_outbox_manifest_v2'
LEGACY_COORDINATION_WRITER_FENCE_SCHEMA: Final[str] = 'loopx_legacy_coordination_writer_fence_v0'
+NEW_GOAL_WRITER_FENCE_SCHEMA: Final[str] = 'loopx_new_goal_writer_fence_v0'
LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA: Final[str] = 'loopx_legacy_coordination_writer_fence_engage_request_v0'
LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA: Final[str] = 'loopx_legacy_coordination_writer_fence_result_v0'
LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA: Final[str] = 'loopx_legacy_coordination_write_check_request_v0'
diff --git a/loopx/control_plane/coordination/coordination_state_contract_v0.json b/loopx/control_plane/coordination/coordination_state_contract_v0.json
index 8b0de9886d..e8fde13ac4 100644
--- a/loopx/control_plane/coordination/coordination_state_contract_v0.json
+++ b/loopx/control_plane/coordination/coordination_state_contract_v0.json
@@ -171,6 +171,7 @@
},
"legacy_writer_fence_protocol": {
"fence_schema": "loopx_legacy_coordination_writer_fence_v0",
+ "creation_fence_schema": "loopx_new_goal_writer_fence_v0",
"engage_request_schema": "loopx_legacy_coordination_writer_fence_engage_request_v0",
"result_schema": "loopx_legacy_coordination_writer_fence_result_v0",
"write_check_request_schema": "loopx_legacy_coordination_write_check_request_v0",
diff --git a/loopx/control_plane/coordination/legacy_writer_fence.ts b/loopx/control_plane/coordination/legacy_writer_fence.ts
index 124fc74f30..2f9558a2b8 100644
--- a/loopx/control_plane/coordination/legacy_writer_fence.ts
+++ b/loopx/control_plane/coordination/legacy_writer_fence.ts
@@ -5,6 +5,7 @@ import { isAbsolute, join } from "node:path";
import type { JsonObject } from "../effect_program.ts";
import { atomicWriteJson, withFileMutationLock } from "../effect_runtime_io.ts";
import { requireJsonObject } from "../runtime_decode.ts";
+import {BARE_SHA256_PATTERN} from "../content_digest.ts";
import { readShadowBootstrapSourcePath, requireShadowPrimaryWriteAllowed, ShadowManagementError, shadowMaintenanceLockPath } from "./shadow_management.ts";
import { legacyCoordinationTodoLockPath, legacyCoordinationLeaseLockPath, taskLeaseLockPath } from "./legacy_writer_lock_paths.ts";
export { legacyCoordinationTodoLockPath, legacyCoordinationLeaseLockPath,
@@ -13,12 +14,14 @@ export { legacyCoordinationTodoLockPath, legacyCoordinationLeaseLockPath,
import {
canonicalAuthorityBytes,
canonicalAuthorityObject,
+ hasExactAuthorityKeys,
requireAuthorityStoreId,
} from "./authority_store_codec.ts";
import {
LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA,
LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA,
LEGACY_COORDINATION_WRITER_FENCE_SCHEMA,
+ NEW_GOAL_WRITER_FENCE_SCHEMA,
LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA,
LEGACY_COORDINATION_WRITE_CHECK_RESULT_SCHEMA,
} from "./coordination_state_contract.generated.ts";
@@ -27,6 +30,7 @@ export {
LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA,
LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA,
LEGACY_COORDINATION_WRITER_FENCE_SCHEMA,
+ NEW_GOAL_WRITER_FENCE_SCHEMA,
LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA,
LEGACY_COORDINATION_WRITE_CHECK_RESULT_SCHEMA,
};
@@ -103,6 +107,18 @@ export function legacyCoordinationWriterFencePath(root: string, goalId: string):
export function decodeLegacyCoordinationWriterFence(value: unknown): JsonObject {
const fence = canonicalAuthorityObject(value, "legacy coordination writer fence");
+ if (fence.schema_version === NEW_GOAL_WRITER_FENCE_SCHEMA) {
+ if (fence.state !== "engaged" || !hasExactAuthorityKeys(fence,
+ ["schema_version", "state", "goal_id", "fence_id", "creation_operation_id", "creation_identity_sha256", "creation_completed"]) ||
+ typeof fence.creation_completed !== "boolean") {
+ throw new Error("Invalid new Goal writer fence");
+ }
+ for (const key of ["goal_id", "fence_id", "creation_operation_id", "creation_identity_sha256"]) {
+ requireAuthorityStoreId(fence[key], `new Goal writer fence ${key}`);
+ }
+ if (!BARE_SHA256_PATTERN.test(String(fence.creation_identity_sha256))) throw new Error("Invalid new Goal creation identity digest");
+ return fence;
+ }
if (
fence.schema_version !== LEGACY_COORDINATION_WRITER_FENCE_SCHEMA ||
fence.state !== "engaged"
@@ -136,6 +152,27 @@ export function decodeLegacyCoordinationWriterFence(value: unknown): JsonObject
}, "legacy coordination writer fence");
}
+/** Record verified creation completion without reopening the legacy writer.
+ * Caller holds the same maintenance/source locks through receipt readback.
+ */
+export async function completeNewGoalWriterFenceUnderLocks(root: string, goalId: string, pending: JsonObject): Promise {
+ const path = legacyCoordinationWriterFencePath(root, goalId);
+ await withFileMutationLock(path, async () => {
+ const current = await loadLegacyCoordinationWriterFence(root, goalId);
+ if (current.status !== "loaded" || current.fence.schema_version !== NEW_GOAL_WRITER_FENCE_SCHEMA ||
+ !canonicalAuthorityBytes({...current.fence, creation_completed: false}).equals(canonicalAuthorityBytes(pending))) {
+ throw new Error("New Goal creation fence changed before completion");
+ }
+ if (current.fence.creation_completed === true) return;
+ const completed = {...pending, creation_completed: true};
+ await atomicWriteJson(path, completed);
+ const checked = await loadLegacyCoordinationWriterFence(root, goalId);
+ if (checked.status !== "loaded" || !canonicalAuthorityBytes(checked.fence).equals(canonicalAuthorityBytes(completed))) {
+ throw new Error("New Goal creation completion fence readback failed");
+ }
+ });
+}
+
export async function loadLegacyCoordinationWriterFence(
root: string,
goalId: string,
diff --git a/loopx/control_plane/coordination/local_authority_defaults.ts b/loopx/control_plane/coordination/local_authority_defaults.ts
index 0e11528360..2e9d76c48e 100644
--- a/loopx/control_plane/coordination/local_authority_defaults.ts
+++ b/loopx/control_plane/coordination/local_authority_defaults.ts
@@ -3,9 +3,13 @@ import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {requireAuthorityStoreId} from "./authority_store_codec.ts";
import {requireLocalAuthorityRuntimeRoot, selectLocalAuthorityTarget} from "./local_authority_provider.ts";
+import {EXECUTION_HANDOFF_MODES} from "./handoff_mode_vocabulary.ts";
+import {initializeNewGoalAuthority} from "./new_goal_initialization.ts";
const GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v0";
const NEW_GOAL_STORAGE_TARGET_SCHEMA = "loopx_new_goal_storage_target_v0";
+export const CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA = "loopx_goal_storage_defaults_v1";
+export const CANONICAL_NEW_GOAL_STORAGE_TARGET_SCHEMA = "loopx_new_goal_storage_target_v1";
function provider(value: unknown): "file" | "sqlite" {
if (value !== "file" && value !== "sqlite") throw new Error("New Goal storage must be file or sqlite");
return value;
@@ -13,13 +17,30 @@ function provider(value: unknown): "file" | "sqlite" {
export async function manageNewGoalStorage(request: JsonObject): Promise {
if (request.action === "resolve") {
const config = requireJsonObject(request.configuration, "Goal storage defaults");
- if (config.schema_version !== GOAL_STORAGE_DEFAULTS_SCHEMA || Object.keys(config).some(key => !["schema_version", "new_goal_provider"].includes(key))) {
+ const canonical = config.schema_version === CANONICAL_GOAL_STORAGE_DEFAULTS_SCHEMA;
+ const keys = canonical ? ["schema_version", "new_goal_provider", "canonical_creation", "new_goal_handoff_mode"] : ["schema_version", "new_goal_provider"];
+ if ((!canonical && config.schema_version !== GOAL_STORAGE_DEFAULTS_SCHEMA) ||
+ Object.keys(config).some(key => !keys.includes(key)) || keys.some(key => !(key in config))) {
throw new Error("Invalid Goal storage defaults");
}
+ if (canonical && (typeof config.canonical_creation !== "boolean" ||
+ !EXECUTION_HANDOFF_MODES.some(mode => mode === config.new_goal_handoff_mode))) {
+ throw new Error("Canonical creation requires a boolean opt-in and soft_claim or hard_lease policy");
+ }
+ if (canonical && config.canonical_creation === true) return {
+ schema_version: CANONICAL_NEW_GOAL_STORAGE_TARGET_SCHEMA, provider: provider(config.new_goal_provider),
+ handoff_mode: config.new_goal_handoff_mode,
+ };
return {schema_version: NEW_GOAL_STORAGE_TARGET_SCHEMA, provider: provider(config.new_goal_provider)};
}
if (request.action !== "initialize") throw new Error("Unknown new Goal storage action");
const target = requireJsonObject(request.target, "New Goal storage target");
+ if (target.schema_version === CANONICAL_NEW_GOAL_STORAGE_TARGET_SCHEMA) {
+ if (Object.keys(target).some(key => !["schema_version", "provider", "handoff_mode"].includes(key)) ||
+ !EXECUTION_HANDOFF_MODES.some(mode => mode === target.handoff_mode)) throw new Error("Invalid canonical creation target");
+ provider(target.provider);
+ return await initializeNewGoalAuthority(request);
+ }
if (target.schema_version !== NEW_GOAL_STORAGE_TARGET_SCHEMA || Object.keys(target).some(key => !["schema_version", "provider"].includes(key))) {
throw new Error("Invalid new Goal storage target");
}
diff --git a/loopx/control_plane/coordination/new_goal_initialization.ts b/loopx/control_plane/coordination/new_goal_initialization.ts
new file mode 100644
index 0000000000..7eb1daa96b
--- /dev/null
+++ b/loopx/control_plane/coordination/new_goal_initialization.ts
@@ -0,0 +1,106 @@
+/** Fresh local authority creation; existing Goals use reviewed migration.
+ * The original creation receipt remains valid after later Todo writes.
+ */
+import {readFile} from "node:fs/promises";
+import {resolve} from "node:path";
+import type {JsonObject} from "../effect_program.ts";
+import {EffectRuntimeConflictError, EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
+import {canonicalAuthorityBytes, canonicalAuthoritySha256, requireAuthorityStoreId} from "./authority_store_codec.ts";
+import {LocalAuthorityProviderOpenError, openLocalAuthorityStoreHandle, requireLocalAuthorityRuntimeRoot, selectLocalAuthorityTarget} from "./local_authority_provider.ts";
+import {engageLegacyCoordinationWriterFenceUnderLocks, completeNewGoalWriterFenceUnderLocks, loadLegacyCoordinationWriterFence, NEW_GOAL_WRITER_FENCE_SCHEMA} from "./legacy_writer_fence.ts";
+import {readShadowManagementState, withShadowMaintenanceLock} from "./shadow_management.ts";
+import {verifyShadowSourceSnapshot, withShadowSourceLocks, type ShadowRequest} from "./runtime_shadow.ts";
+import {projectCoordinationSource, SOURCE_PROJECTION_REQUEST_SCHEMA} from "./source_projection.ts";
+import {TODO_DOMAIN_READ_RECORD_SCHEMA} from "./coordination_state_contract.ts";
+import {commitPromotionAndReadBack, readPromotionReceipt} from "./promotion_receipt.ts";
+import {requireJsonObject} from "../runtime_decode.ts";
+
+export async function initializeNewGoalAuthority(raw: JsonObject): Promise {
+ const root = requireLocalAuthorityRuntimeRoot(raw.runtime_root);
+ const goalId = requireAuthorityStoreId(raw.goal_id, "goal id");
+ const operationId = requireAuthorityStoreId(raw.creation_operation_id, "creation operation id");
+ const target = requireJsonObject(raw.target, "creation target");
+ const snapshot = requireJsonObject(raw.source_snapshot, "creation source snapshot");
+ const projection = projectCoordinationSource({schema_version: SOURCE_PROJECTION_REQUEST_SCHEMA,
+ kind: "snapshot", goal_id: goalId, handoff_mode: target.handoff_mode, todos: [], leases: [],
+ read_model_schema: TODO_DOMAIN_READ_RECORD_SCHEMA}).projection as JsonObject;
+ const capturedSource = raw.projection !== undefined;
+ const request: ShadowRequest = {runtime_root: root, goal_id: goalId,
+ projection: capturedSource ? requireJsonObject(raw.projection, "creation source") : projection, source_snapshot: snapshot};
+ const identitySha = canonicalAuthoritySha256({goal_id: goalId, creation_operation_id: operationId,
+ target, state_path: snapshot.state_path, registry_path: (snapshot.registry_source as JsonObject)?.path});
+ const identity = {operation_id: operationId, projection_sha256: canonicalAuthoritySha256(projection),
+ receipt: {schema_version: "loopx_new_goal_authority_creation_receipt_v0", operation_id: operationId,
+ goal_id: goalId, creation_identity_sha256: identitySha}};
+ const fence = {schema_version: NEW_GOAL_WRITER_FENCE_SCHEMA, state: "engaged", goal_id: goalId,
+ fence_id: `new-goal:${operationId}`, creation_operation_id: operationId, creation_identity_sha256: identitySha,
+ creation_completed: false};
+ const validateRegistration = async () => {
+ const registry = JSON.parse(await readFile(String((snapshot.registry_source as JsonObject).path), "utf8"));
+ const registered = registry.goals?.find((goal: JsonObject) => goal.id === goalId);
+ if (!registered || registered.creation_operation_id !== operationId ||
+ !canonicalAuthorityBytes(registered.coordination?.storage_target).equals(canonicalAuthorityBytes(target)) ||
+ typeof registered.repo !== "string" || typeof registered.state_file !== "string" ||
+ resolve(registered.repo, registered.state_file) !== resolve(String(snapshot.state_path))) {
+ throw new EffectRuntimeRequestError("Canonical creation is not bound to the registered original operation, source and target");
+ }
+ };
+ await validateRegistration();
+ const priorFence = await loadLegacyCoordinationWriterFence(root, goalId);
+ if (priorFence.status === "failed") throw new EffectRuntimeConflictError(priorFence.reason);
+ if (capturedSource && priorFence.status === "missing" && ((request.projection.todos as unknown[])?.length !== 0 ||
+ (request.projection.leases as unknown[])?.length !== 0 || (snapshot.lease_inventory as unknown[])?.length !== 0)) {
+ throw new EffectRuntimeRequestError("Canonical creation requires an empty source without lease history; use reviewed migration");
+ }
+
+ // Selection retains the established provider/lineage checks. It cannot replace
+ // a selected store, and later reviewed migration wins over creation defaults.
+ if (capturedSource) await selectLocalAuthorityTarget(root, goalId, target.provider as "file" | "sqlite", true, "creation_retry");
+ return await withShadowMaintenanceLock(root, goalId, () => withShadowSourceLocks(request, async () => {
+ await validateRegistration();
+ const managed = await readShadowManagementState(root, goalId);
+ if (managed?.status === "active") throw new EffectRuntimeConflictError("Existing shadow capture requires reviewed migration");
+ const opened = await openLocalAuthorityStoreHandle(root, goalId).catch((error: unknown) => {
+ if (error instanceof LocalAuthorityProviderOpenError) {
+ throw new EffectRuntimeConflictError(`${error.message}; restore the complete authority backup, never recreate it`);
+ }
+ throw error;
+ });
+ const head = await opened.store.loadAuthority();
+ const persisted = await loadLegacyCoordinationWriterFence(root, goalId);
+ if (persisted.status === "failed") throw new EffectRuntimeConflictError(persisted.reason);
+ if (persisted.status === "loaded" && !canonicalAuthorityBytes({...persisted.fence, creation_completed: false}).equals(canonicalAuthorityBytes(fence))) {
+ throw new EffectRuntimeConflictError("Canonical creation writer fence belongs to a different operation");
+ }
+ let readback = await readPromotionReceipt(opened.store, identity);
+ if (!readback.matched) {
+ if (persisted.status === "loaded" && persisted.fence.creation_completed === true) {
+ throw new EffectRuntimeConflictError("Completed canonical creation authority is unavailable; restore its complete backup, never recreate it");
+ }
+ if (head.status !== "missing") throw new EffectRuntimeConflictError(`Canonical creation cannot replace authority: ${readback.reason_code}`);
+ if (!capturedSource) return {source_capture_required: true};
+ // A creation intent is never a migration waiver. Validate the complete
+ // real source and inventory under its locks; nonempty sources fail closed.
+ await verifyShadowSourceSnapshot(request);
+ if ((request.projection.todos as unknown[])?.length !== 0 ||
+ (request.projection.leases as unknown[])?.length !== 0 ||
+ (snapshot.lease_inventory as unknown[])?.length !== 0) {
+ throw new EffectRuntimeRequestError("Canonical creation requires an empty source without lease history; use reviewed migration");
+ }
+ const fenced = await engageLegacyCoordinationWriterFenceUnderLocks(root, goalId, String(snapshot.state_path), fence);
+ if (fenced.status !== "applied" && fenced.status !== "replayed") throw new EffectRuntimeConflictError("Canonical creation writer fence could not be verified");
+ const committed = await commitPromotionAndReadBack(opened.store, identity, projection,
+ {...identity.receipt, schema_version: "loopx_new_goal_authority_creation_event_v0"});
+ readback = committed.readback;
+ if (!readback.matched) throw new EffectRuntimeConflictError(`Canonical creation interrupted; retry its original operation: ${readback.reason_code}`);
+ }
+ if (persisted.status === "missing" && head.status === "loaded") {
+ throw new EffectRuntimeConflictError("Canonical creation receipt exists but its writer fence is missing; restore the complete authority backup");
+ }
+ await completeNewGoalWriterFenceUnderLocks(root, goalId, fence);
+ return {ok: true, provider: opened.provider, status: head.status === "missing" ? "created" : "replayed",
+ authority_initialized: true, handoff_mode: target.handoff_mode, applies_to: "canonical_authority",
+ promotion_performed: false, legacy_writer_fenced: true, legacy_fallback_used: false,
+ provider_revision: readback.provider_revision, cursor: readback.cursor, creation_operation_id: operationId};
+ }));
+}
diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json
index 447353acd5..3bc84e714b 100644
--- a/loopx/semantics/project_registry_io_manifest_v1.json
+++ b/loopx/semantics/project_registry_io_manifest_v1.json
@@ -111,7 +111,7 @@
},
{
"site": "loopx/bootstrap.py::.bootstrap_project::codec_transaction:project_registry_transaction#1",
- "line": 527,
+ "line": 544,
"column": 14,
"kind": "codec_transaction",
"api": "project_registry_transaction",
@@ -119,7 +119,7 @@
},
{
"site": "loopx/bootstrap.py::.read_json_if_exists::codec_read:load_project_registry#1",
- "line": 84,
+ "line": 86,
"column": 15,
"kind": "codec_read",
"api": "load_project_registry",
diff --git a/scripts/generate_coordination_state_contract.py b/scripts/generate_coordination_state_contract.py
index 66a503c8af..ec494cbde8 100644
--- a/scripts/generate_coordination_state_contract.py
+++ b/scripts/generate_coordination_state_contract.py
@@ -85,6 +85,7 @@
)
LEGACY_WRITER_FENCE_PROTOCOL_KEYS = (
"fence_schema",
+ "creation_fence_schema",
"engage_request_schema",
"result_schema",
"write_check_request_schema",
@@ -150,6 +151,7 @@
)
LEGACY_WRITER_FENCE_CONSTANT_NAMES = {
"fence_schema": "LEGACY_COORDINATION_WRITER_FENCE_SCHEMA",
+ "creation_fence_schema": "NEW_GOAL_WRITER_FENCE_SCHEMA",
"engage_request_schema": "LEGACY_COORDINATION_WRITER_FENCE_ENGAGE_REQUEST_SCHEMA",
"result_schema": "LEGACY_COORDINATION_WRITER_FENCE_RESULT_SCHEMA",
"write_check_request_schema": "LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA",
diff --git a/tests/control_plane/test_new_goal_storage_defaults.py b/tests/control_plane/test_new_goal_storage_defaults.py
index c77aec63f2..b5531396f0 100644
--- a/tests/control_plane/test_new_goal_storage_defaults.py
+++ b/tests/control_plane/test_new_goal_storage_defaults.py
@@ -5,6 +5,7 @@
import subprocess
import sys
import threading
+from pathlib import Path
from unittest.mock import patch
import pytest
@@ -24,14 +25,17 @@ def environment(tmp_path, monkeypatch):
project.mkdir()
config = runtime / "machine/configuration.json"
config.parent.mkdir(parents=True)
- def configure(provider):
+ def configure(provider, *, mode=None):
+ defaults = {"schema_version": "loopx_goal_storage_defaults_v0", "new_goal_provider": provider}
+ if mode is not None:
+ defaults.update(schema_version="loopx_goal_storage_defaults_v1", canonical_creation=True, new_goal_handoff_mode=mode)
config.write_text(json.dumps({"schema_version": "loopx_machine_configuration_v0", "namespaces": {
- "goal_storage": {"schema_version": "loopx_goal_storage_defaults_v0", "new_goal_provider": provider}}}))
- def bootstrap(goal="first", *extra):
+ "goal_storage": defaults}}))
+ def bootstrap(goal="first", *extra, expected_code=0):
result = subprocess.run([sys.executable, "-m", "loopx.entrypoint", "--registry", str(project / ".loopx/registry.json"),
"--runtime-root", str(runtime), "--format", "json", "bootstrap", "--project", str(project), "--goal-id", goal,
"--objective", "Validate a new project", "--no-global-sync", *extra], capture_output=True, text=True, timeout=60)
- assert result.returncode == 0, result.stdout + result.stderr
+ assert result.returncode == expected_code, result.stdout + result.stderr
return json.loads(result.stdout)
def marker(goal="first"):
return runtime / "authority" / f"provider-{hashlib.sha256(goal.encode()).hexdigest()}.json"
@@ -62,6 +66,103 @@ def test_existing_implicit_file_goal_is_not_retargeted(environment):
assert not marker().exists()
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+@pytest.mark.parametrize("mode", ["soft_claim", "hard_lease"])
+def test_opted_in_creation_has_complete_canonical_authority_and_frozen_policy(environment, provider, mode):
+ from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted
+
+ configure, bootstrap, _, _, runtime = environment
+ configuration = runtime / "machine/configuration.json"
+ configuration.write_text(json.dumps({"schema_version": "loopx_machine_configuration_v0", "namespaces": {
+ "goal_storage": {"schema_version": "loopx_goal_storage_defaults_v1", "new_goal_provider": provider,
+ "canonical_creation": True, "new_goal_handoff_mode": mode}}}))
+ preview = bootstrap("native", "--dry-run")
+ assert not (runtime / "authority-transition").exists()
+ actual = bootstrap("native")
+ source = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="native", include_leases=True)
+ assert source is not None
+ assert source["source_authority"] == f"{provider}_v0"
+ assert source["handoff_mode"] == mode
+ assert source["todos"] == []
+ assert actual["storage_selection"]["authority_initialized"] is True
+ assert actual["storage_target"] == preview["storage_target"]
+ configure("file" if provider == "sqlite" else "sqlite")
+ restart_effect_runtime()
+ reconnect = bootstrap("native")
+ assert reconnect["storage_selection"]["authority_initialized"] is True
+ assert reconnect["storage_selection"]["handoff_mode"] == mode
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+@pytest.mark.parametrize("mode", ["soft_claim", "hard_lease"])
+@pytest.mark.parametrize("compatibility_source", ["missing", "unreadable"])
+def test_completed_cli_creation_replays_without_compatibility_source(environment, provider, mode, compatibility_source):
+ from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted
+
+ configure, bootstrap, _, project, runtime = environment
+ configure(provider, mode=mode)
+ created = bootstrap()
+ registry = project / ".loopx/registry.json"
+ added = subprocess.run([sys.executable, "-m", "loopx.entrypoint", "--registry", str(registry),
+ "--runtime-root", str(runtime), "--format", "json", "todo", "add", "--goal-id", "first",
+ "--role", "agent", "--text", "Preserve a later native Todo"], capture_output=True, text=True, timeout=60)
+ assert added.returncode == 0, added.stdout + added.stderr
+ before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="first", include_leases=True)
+ assert len(before["todos"]) == 1
+ state = Path(created["state_file"])
+ if compatibility_source == "missing":
+ state.unlink()
+ else:
+ state.write_bytes(b"\xff")
+ configure("file" if provider == "sqlite" else "sqlite", mode="hard_lease" if mode == "soft_claim" else "soft_claim")
+ restart_effect_runtime()
+ recovered = bootstrap()
+ selection = recovered["storage_selection"]
+ assert selection["creation_operation_id"] == created["storage_selection"]["creation_operation_id"]
+ assert selection["provider_revision"] == created["storage_selection"]["provider_revision"]
+ assert selection["handoff_mode"] == mode
+ assert selection["provider"] == provider
+ assert selection["legacy_fallback_used"] is False
+ assert recovered["state_action"] == "kept-existing"
+ assert next(action for action in recovered["actions"] if action["path"] == str(state))["action"] == "kept-existing"
+ assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="first", include_leases=True) == before
+ assert not state.exists() if compatibility_source == "missing" else state.read_bytes() == b"\xff"
+ assert bootstrap("first", "--dry-run")["state_action"] == "kept-existing"
+ rejected = bootstrap("first", "--force", expected_code=1)
+ assert "cannot rebuild" in rejected["error"]
+ assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="first", include_leases=True) == before
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+@pytest.mark.parametrize("failure", ["missing_authority", "changed_operation", "changed_source"])
+def test_cli_creation_recovery_cannot_recreate_or_adopt_authority(environment, provider, failure):
+ configure, bootstrap, _, project, runtime = environment
+ configure(provider, mode="hard_lease")
+ created = bootstrap()
+ restart_effect_runtime()
+ state = Path(created["state_file"])
+ state.unlink()
+ digest = hashlib.sha256(b"first").hexdigest()
+ authority = runtime / "authority" / f"{provider}-v0" / (
+ f"authority-{digest}.sqlite" if provider == "sqlite" else f"authority-store-{digest[:16]}.json")
+ before = authority.read_bytes()
+ if failure == "missing_authority":
+ authority.rename(authority.with_suffix(".unavailable"))
+ elif failure == "changed_operation":
+ registry = project / ".loopx/registry.json"
+ data = json.loads(registry.read_text())
+ data["goals"][0]["creation_operation_id"] = "another-creation-operation"
+ registry.write_text(json.dumps(data))
+ wrong_source = project / "another-source.md"
+ rejected = bootstrap("first", *( ["--state-file", str(wrong_source)] if failure == "changed_source" else []), expected_code=1)
+ assert rejected["ok"] is False
+ expected_error = {"missing_authority": "restore", "changed_operation": "different operation", "changed_source": "not bound"}[failure]
+ assert expected_error in rejected["error"], rejected
+ assert not authority.exists() if failure == "missing_authority" else authority.read_bytes() == before
+ assert not state.exists()
+ assert not wrong_source.exists()
+
+
def test_pending_creation_uses_frozen_intent_after_machine_default_changes(environment):
configure, bootstrap, marker, project, _ = environment
# Independently model the durable boundary: registry/state published, selector absent.
@@ -113,13 +214,14 @@ def request(path, body):
@pytest.mark.parametrize("provider", ["file", "sqlite"])
@pytest.mark.parametrize("relative_runtime", [False, True])
-def test_app_creation_retries_storage_before_reporting_success(environment, app, monkeypatch, provider, relative_runtime):
+@pytest.mark.parametrize("mode", [None, "soft_claim", "hard_lease"])
+def test_app_creation_retries_storage_before_reporting_success(environment, app, monkeypatch, provider, relative_runtime, mode):
from loopx.capabilities.machine_configuration import goal_storage
from loopx.todos import add_goal_todo
configure, _, marker, project, _ = environment
store, request = app
- configure(provider)
+ configure(provider, mode=mode)
registry = project / ".loopx/registry.json"
# Registry-relative runtime routing must agree with CLI bootstrap, regardless
# of the HTTP server process's working directory.
@@ -177,6 +279,14 @@ def storage_effect(method, payload):
assert recovered["proposal"]["receipt"]["outcome"] == "goal_created"
assert selections[-1]["provider"] == provider
assert selections[-1]["promotion_performed"] is False
+ if mode is not None:
+ from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted
+ source = read_canonical_todos_if_promoted(runtime_root=project.parent / "runtime", goal_id="recovery", include_leases=True)
+ assert source["handoff_mode"] == mode
+ assert len(source["todos"]) == 1
+ assert source["todos"][0]["text"] == "Verify recovery"
+ assert source["source_authority"] == f"{provider}_v0"
+ assert selections[-1]["authority_initialized"] is True
if provider == "sqlite":
assert json.loads(marker("recovery").read_text())["provider"] == provider
else:
@@ -184,7 +294,20 @@ def storage_effect(method, payload):
assert add.call_count == 1
assert request(apply_path, {})[1]["proposal"]["receipt"] == recovered["proposal"]["receipt"]
assert add.call_count == 1
- assert len(selections) == 1
+ assert len(selections) == (1 if mode is None else 2)
+
+
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
+def test_canonical_creation_force_rebuild_is_rejected_without_changing_todos(environment, provider):
+ configure, bootstrap, _, project, runtime = environment
+ configure(provider, mode="hard_lease")
+ created = bootstrap()
+ state = Path(created["state_file"])
+ before = state.read_bytes()
+ rejected = bootstrap("first", "--force", expected_code=1)
+ assert "cannot rebuild" in rejected["error"]
+ assert state.read_bytes() == before
+ assert created["storage_selection"]["legacy_writer_fenced"] is True
@pytest.mark.parametrize("provider", ["file", "sqlite"])
diff --git a/tests/control_plane_ts/content_digest_single_owner.test.ts b/tests/control_plane_ts/content_digest_single_owner.test.ts
index 1992d56fe8..569e2ae329 100644
--- a/tests/control_plane_ts/content_digest_single_owner.test.ts
+++ b/tests/control_plane_ts/content_digest_single_owner.test.ts
@@ -100,6 +100,7 @@ const CANONICAL_CONSUMERS = [
"control_plane/collaboration/semantic_request.ts",
"control_plane/coordination/authority_archive_read.ts",
"control_plane/coordination/authority_source.ts",
+ "control_plane/coordination/legacy_writer_fence.ts",
"control_plane/coordination/local_authority_migration.ts",
"control_plane/coordination/local_authority_shadow.ts",
"control_plane/coordination/local_authority_shadow_outbox.ts",
diff --git a/tests/control_plane_ts/new_goal_initialization.test.ts b/tests/control_plane_ts/new_goal_initialization.test.ts
new file mode 100644
index 0000000000..8cd1d1ff56
--- /dev/null
+++ b/tests/control_plane_ts/new_goal_initialization.test.ts
@@ -0,0 +1,111 @@
+import assert from "node:assert/strict";
+import {test} from "node:test";
+import {createHash} from "node:crypto";
+import {mkdtemp, writeFile, readFile, rm} from "node:fs/promises";
+import {tmpdir} from "node:os";
+import {join} from "node:path";
+import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
+import {manageNewGoalStorage} from "../../loopx/control_plane/coordination/local_authority_defaults.ts";
+import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts";
+import {SqliteAuthorityStore} from "../../loopx/control_plane/coordination/sqlite_authority_store.ts";
+import {openLocalAuthorityStore, localAuthorityProviderPaths} from "../../loopx/control_plane/coordination/local_authority_provider.ts";
+import {loadLegacyCoordinationWriterFence, checkLegacyCoordinationWriteAllowed, LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA} from "../../loopx/control_plane/coordination/legacy_writer_fence.ts";
+import {projectCoordinationSource, SOURCE_PROJECTION_REQUEST_SCHEMA} from "../../loopx/control_plane/coordination/source_projection.ts";
+import {TODO_DOMAIN_READ_RECORD_SCHEMA} from "../../loopx/control_plane/coordination/coordination_state_contract.ts";
+import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts";
+
+async function fixture(provider: "file" | "sqlite") {
+ const root = await mkdtemp(join(tmpdir(), "loopx-new-authority-"));
+ const state = join(root, "state.md"), registryPath = join(root, "registry.json");
+ const bytes = "---\ngoal_id: new-goal\nhandoff_mode: hard_lease\n---\n# Goal\n\n## Agent Todo\n";
+ const target = {schema_version: "loopx_new_goal_storage_target_v1", provider, handoff_mode: "hard_lease"};
+ const registry = {common_runtime_root: root, goals: [{id: "new-goal", repo: root, state_file: "state.md",
+ creation_operation_id: "create-native", coordination: {storage_target: target}}]};
+ const registryBytes = JSON.stringify(registry);
+ await writeFile(state, bytes); await writeFile(registryPath, registryBytes);
+ const projection = projectCoordinationSource({schema_version: SOURCE_PROJECTION_REQUEST_SCHEMA, kind: "snapshot",
+ goal_id: "new-goal", handoff_mode: "hard_lease", todos: [], leases: [], read_model_schema: TODO_DOMAIN_READ_RECORD_SCHEMA}).projection as JsonObject;
+ const sha = (value: string) => createHash("sha256").update(value).digest("hex");
+ const request: JsonObject = {action: "initialize", runtime_root: root, goal_id: "new-goal", target,
+ creation_operation_id: "create-native", projection, source_snapshot: {state_path: state, registered_state_path: state,
+ registered_runtime_root: root, state_bytes_sha256: `sha256:${sha(bytes)}`, lease_inventory: [],
+ projection_sha256: canonicalAuthoritySha256(projection), evidence_files: [],
+ registry_source: {path: registryPath, sha256: sha(registryBytes), registered_agents: []}}};
+ return {root, state, registryPath, request};
+}
+
+for (const provider of ["file", "sqlite"] as const) {
+ test(`${provider}: interrupted creation fences old writers and retries the original native receipt`, async () => {
+ const f = await fixture(provider), Store = provider === "file" ? FileAuthorityStore : SqliteAuthorityStore;
+ const commit = Store.prototype.commitAuthority;
+ try {
+ Store.prototype.commitAuthority = async () => {throw new Error("lost before commit");};
+ await assert.rejects(manageNewGoalStorage(f.request), /interrupted/);
+ const blocked = await checkLegacyCoordinationWriteAllowed({schema_version: LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA,
+ runtime_root: f.root, goal_id: "new-goal"});
+ assert.equal(blocked.status, "blocked");
+ Store.prototype.commitAuthority = commit;
+ const recovered = await manageNewGoalStorage(f.request);
+ assert.equal(recovered.authority_initialized, true);
+ const fence = await loadLegacyCoordinationWriterFence(f.root, "new-goal");
+ assert.equal(fence.status, "loaded"); if (fence.status === "loaded") assert.equal(fence.fence.creation_completed, true);
+ const store = await openLocalAuthorityStore(f.root, "new-goal"), loaded = await store.loadAuthority();
+ assert.equal(loaded.status, "loaded"); if (loaded.status !== "loaded") return;
+ await store.commitAuthority({expected_provider_revision: loaded.provider_revision, operation_id: "later-write",
+ next_projection: {...loaded.head, progress: "later native write"}, events: [], receipts: []});
+ const replay = await manageNewGoalStorage(f.request);
+ assert.equal(replay.cursor, "1", "read the original creation receipt after later commits");
+ const after = await store.loadAuthority();
+ assert.equal(after.status, "loaded"); if (after.status === "loaded") assert.equal(after.head.progress, "later native write");
+ const {projection: _projection, ...receiptRequest} = f.request;
+ await rm(f.state);
+ assert.equal((await manageNewGoalStorage(receiptRequest)).authority_initialized, true,
+ "completed native receipt must not depend on Markdown source parsing or availability");
+ assert.equal((await store.scanCommitted(null, 10)).status, "page");
+ } finally {Store.prototype.commitAuthority = commit; await rm(f.root, {recursive: true, force: true});}
+ });
+ test(`${provider}: foreign operation and nonempty source do not grant creation or change selection`, async () => {
+ const f = await fixture(provider);
+ try {
+ await assert.rejects(manageNewGoalStorage({...f.request, creation_operation_id: "foreign"}), /original operation/);
+ const snapshot = f.request.source_snapshot as JsonObject;
+ await assert.rejects(manageNewGoalStorage({...f.request, source_snapshot: {...snapshot,
+ lease_inventory: [{name: "retained.json", bytes_sha256: "sha256:retained"}]}}), /lease history/);
+ assert.equal((await loadLegacyCoordinationWriterFence(f.root, "new-goal")).status, "missing");
+ await assert.rejects(readFile(localAuthorityProviderPaths(f.root, "new-goal").marker), {code: "ENOENT"});
+ await writeFile(f.state, "changed source");
+ await assert.rejects(manageNewGoalStorage(f.request), /source_changed_retry/);
+ assert.equal((await loadLegacyCoordinationWriterFence(f.root, "new-goal")).status, "missing");
+ } finally {await rm(f.root, {recursive: true, force: true});}
+ });
+}
+
+test("creation opt-in reuses execution policy vocabulary; legacy is only a compatibility target", async () => {
+ await assert.rejects(manageNewGoalStorage({action: "resolve", configuration: {schema_version: "loopx_goal_storage_defaults_v1",
+ new_goal_provider: "sqlite", canonical_creation: true, new_goal_handoff_mode: "legacy"}}), /soft_claim or hard_lease/);
+ assert.deepEqual(await manageNewGoalStorage({action: "resolve", configuration: {schema_version: "loopx_goal_storage_defaults_v1",
+ new_goal_provider: "sqlite", canonical_creation: false, new_goal_handoff_mode: "hard_lease"}}),
+ {schema_version: "loopx_new_goal_storage_target_v0", provider: "sqlite"});
+});
+
+test("completed File creation cannot recreate a lost authority document", async () => {
+ const f = await fixture("file");
+ try {
+ await manageNewGoalStorage(f.request);
+ const store = await openLocalAuthorityStore(f.root, "new-goal");
+ assert.ok(store instanceof FileAuthorityStore);
+ await rm(store.path);
+ await assert.rejects(manageNewGoalStorage(f.request), /restore its complete backup/);
+ await assert.rejects(readFile(store.path), {code: "ENOENT"});
+ } finally {await rm(f.root, {recursive: true, force: true});}
+});
+
+test("unfinished creation preflight requests source capture without selecting a provider or fencing writers", async () => {
+ const f = await fixture("sqlite");
+ try {
+ const {projection: _projection, ...request} = f.request;
+ assert.deepEqual(await manageNewGoalStorage(request), {source_capture_required: true});
+ assert.equal((await loadLegacyCoordinationWriterFence(f.root, "new-goal")).status, "missing");
+ await assert.rejects(readFile(localAuthorityProviderPaths(f.root, "new-goal").marker), {code: "ENOENT"});
+ } finally {await rm(f.root, {recursive: true, force: true});}
+});