Compare commits

...
3 changed files with 777 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
# AIOS V2 Implementation Status
- 扫描日期:2026-07-20
- 当前发布基线:`V1R4C6`
- 基线提交:`e204a90398bf646d46245b3088226749fa056edd`
- 实施分支:`patch/aios-v2-implementation-status-next-phase-20260720`
- 决策角色:AIOS V2 CTO
- 总体判定:**进入主线收敛阶段,不重写现有系统。**
## 1. 执行摘要
AIOS V2 已经具备一批可复用的候选实现,包括签名租户上下文、权威 Tenant Router、Tenant Cell 生命周期、OpenClaw Fleet 适配、Hermes 租户队列与 Profile 隔离、消息账本、内存/密钥作用域以及追加式审计链。GitOps Runtime 当前可用,分支、提交和审计元数据机制可以继续作为所有变更的唯一入口。
当前尚不能宣称 AIOS V2 已交付。`V1R4C6` 的提升理由仍注明未部署生产;V2R1 实现主要存在于候选发布、集成、任务和结果分支,未进入 `main` 或当前基线。请求的 `docs/aios-v2/` 权威设计目录在已核对的 `main`、当前治理分支和抽查的 V2R1 集成/设计分支中未发现,因此架构目标尚未形成可被自动验证的单一事实源。
CTO 决策:
1. 不推倒 OpenClaw、Hermes、Agent Router 和现有 GitOps 能力。
2. AIOS 收敛为唯一控制面;OpenClaw 变为执行适配器;Hermes 变为租户隔离的 Runtime;Agent Router 变为统一调度与路由平面。
3. 候选能力先做证据映射、边界硬化和小步集成,再进入基线。
4. 所有 API、控制能力、运行状态与审计查询必须 Web 化。
5. 所有模块必须在入口、存储、队列、缓存、密钥、日志、审计和前端会话层显式携带并校验 `tenant_id`。
6. 所有修改必须通过非 `main` 分支、可审查提交、自动测试和受控提升完成。
## 2. 扫描范围与证据边界
### 已读取
- GitOps Runtime 状态与当前基线。
- `main`、V2R1 候选发布分支、集成/任务/结果分支的定向差异。
- `release/v2r1c0-tenant-cell-fabric-20260712` 中的 `active-runtime/app/aios_v2r1` 候选实现。
- 同一候选分支中的 `tests/v2r1` 合同测试与发布门禁说明。
- Gateway、自动编排、租户隔离、WebUI、事件总线等相关分支和结果分支的提交证据。
### 限制
- `docs/aios-v2/` 未出现在已核对的权威分支,无法完成“对该目录全部文件逐份比对”。本报告将此项列为 P0,而不是假设文档已经存在。
- 从 `V1R4C6` 到 `main`、以及部分大型任务分支的累计差异超过 GitOps diff 接口上限。因此本次采用定向目录、最近提交和结果证据扫描;下一阶段必须生成机器可读的能力—提交—测试索引。
- “分支存在”不等于“能力已合入”;“测试报告存在”不等于“生产门禁已通过”。状态必须区分基线、候选、验证和部署。
## 3. 当前架构扫描
### 3.1 权威基线
| 项目 | 当前状态 | 结论 |
|---|---|---|
| 发布基线 | `V1R4C6` | 当前唯一被提升的版本 |
| 生产部署 | 未确认;基线理由注明未部署生产 | 不得视为生产完成 |
| GitOps Runtime | Active | 可作为唯一变更通道 |
| V2R1 代码进入 `main` | 未发现 | 候选能力尚未成为权威实现 |
| `docs/aios-v2/` | 未发现 | P0 文档与治理缺口 |
### 3.2 候选运行时能力
`release/v2r1c0-tenant-cell-fabric-20260712` 中已发现以下候选模块:
| 模块 | 已有能力 | 当前成熟度 |
|---|---|---|
| `tenant_context.py` | HMAC 签名上下文、规范化消息模型、来源校验、基础工具策略 | 候选;缺少密钥轮换、重放防护和集中策略版本 |
| `router.py` | SQLite 权威租户、渠道、主体和会话路由;fail-closed 校验 | 候选;需统一到控制面并替换单机存储假设 |
| `cell_manager.py` / `cli.py` | Tenant Cell 注册和状态操作 | 原型;JSON 注册表无并发控制,状态机约束不足 |
| `fleet.py` | OpenClaw Fleet 命令适配 | 可复用适配层雏形;缺少超时、认证、幂等、审计和能力协商 |
| `hermes_fabric.py` | 强制租户分区的任务队列、来源限制、领取接口 | 候选;共享 SQLite 仍是逻辑隔离,需生产级队列和配额 |
| `hermes_profile.py` | HOME、工作区和路径逃逸防护 | 可复用;需容器/进程级隔离证据 |
| `memory_scope.py` | session/profile/cell/tenant/shared artifact 命名空间 | 可复用合同;需绑定实际存储后端和查询层 |
| `secret_scope.py` | 仅保存 SecretRef,校验 tenant/cell 所有权 | 可复用合同;尚无真实 broker、轮换和撤销流程 |
| `message_ledger.py` | 精确消息 ID、TTL、冲突检测和引用解析 | 候选;需迁移、容量、加密、备份和多实例并发验证 |
| `audit.py` | 追加式哈希链 | 原型;当前链为全局序列,应按租户/审计域隔离并支持外部不可变存储 |
### 3.3 测试与门禁
已发现可执行合同测试,覆盖 Tenant Router 隔离、TenantContext 签名和 Hermes 来源/重复事件处理。候选验收清单明确要求:
- 跨租户内存、Cell、密钥和队列访问必须拒绝。
- 非当前已认证人工命令不得触发副作用。
- OpenClaw Runtime Hook 与副作用审批属于必需项。
- 在可执行测试与运行时证据通过之前,不得生产部署。
因此当前标记为“候选能力存在”,而不是“发布完成”。
### 3.4 控制面与自动化
结果分支显示 Gateway 和自动编排已出现较强测试证据,包括单元/集成测试与 Codex 任务领取、生命周期回写能力。但是这些成果仍分散在结果/集成分支,尚未形成 AIOS 唯一入口,也未完成从任务结果到权威基线的提升闭环。
### 3.5 Web 化现状
仓库存在 WebUI、观测与控制面相关分支,但分支名称和最后提交内容有不一致现象。当前不能仅依据分支名认定功能完成。需要建立 API 路由、页面、权限、租户隔离、E2E 测试和部署证据五项映射。
## 4. 与 AIOS V2 目标架构对比
目标逻辑架构:
```text
Web Console / Public API
|
AIOS Control Plane
- Tenant Registry
- Identity & Policy
- Agent Registry
- Workflow / Task Lifecycle
- Approval & Audit
- Runtime Inventory
|
Agent Router / Scheduling Plane
/ \
OpenClaw Adapter Hermes Runtime Adapter
| |
OpenClaw Cells Hermes Workers / Profiles
```
边界规则:
- 只有 AIOS Control Plane 可以创建、审批、取消和审计任务。
- Router 只负责基于权威上下文的选择、调度和转发,不拥有租户事实源。
- OpenClaw 不拥有租户、策略或任务生命周期;只执行有签名、可审计、可幂等的命令。
- Hermes 不拥有控制面策略;只运行隔离 Profile、消费租户队列并返回结果/遥测。
- Web Console 不直接调用 OpenClaw/Hermes;只调用 AIOS API。
- 每次调用均传播 `tenant_id`、`principal_id`、`session_id`、`task_id`、`trace_id` 和策略版本。
## 5. Gap Analysis
| ID | 领域 | 当前 | 目标 | Gap | 优先级 |
|---|---|---|---|---|---|
| G-001 | 架构事实源 | `docs/aios-v2/` 不在权威分支 | 全部 V2 设计、ADR、接口和门禁可追踪 | 无法自动核对目标架构 | P0 |
| G-002 | 主线收敛 | V2 能力分散于候选/结果分支 | `main` 上单一集成历史 | 分支存在但未纳管、未提升 | P0 |
| G-003 | 唯一控制面 | Gateway/Router/执行器均可能承载控制逻辑 | AIOS 是唯一任务与策略入口 | 控制权边界尚未强制 | P0 |
| G-004 | 多租户强制 | 候选模块多为显式字段和 SQLite 逻辑隔离 | 全链路强制隔离、负向测试、生产存储策略 | 缺少统一 middleware、RLS/物理隔离策略和 E2E 证据 | P0 |
| G-005 | 审计与重放 | 全局哈希链;上下文有 nonce 但无 replay store | 按租户/审计域不可篡改、可查询、可验证;命令防重放 | 审计域、保留、导出、重放防护不足 | P0 |
| G-006 | OpenClaw 边界 | Fleet CLI 适配已有 | 版本化执行协议、幂等、超时、取消、回调、审计 | 仍可能被控制面之外直接调用 | P1 |
| G-007 | Hermes 边界 | 租户队列/Profile 已有 | Runtime-only、配额、公平调度、取消、恢复 | 共享存储和控制逻辑仍需拆分 | P1 |
| G-008 | Web 化 | 多个局部 Web/UI 分支 | 所有控制能力均有 Web/API,权限一致 | 无统一 Console、API contract 和 E2E 门禁 | P1 |
| G-009 | 生产数据层 | 多个候选模块使用 SQLite/JSON | HA、迁移、备份、加密、容量和恢复目标 | 原型数据层不能直接生产 | P1 |
| G-010 | 证据治理 | 分支名与内容可能不一致,大 diff 超限 | 能力—提交—测试—部署机器可读映射 | 无法可靠计算完成度 | P0 |
| G-011 | 发布闭环 | 基线提升与生产部署脱节 | 构建、测试、安全、审批、提升、部署、回滚闭环 | 当前基线仍注明未部署 | P0 |
| G-012 | 可观测性 | 局部分支存在观测/WebUI 工作 | tenant/task/agent/trace 统一指标日志追踪 | 统一 telemetry contract 尚未成为门禁 | P1 |
## 6. 能力标记
### 6.1 保留并复用
- GitOps Runtime、非主线分支、提交元数据、审批式 Tag/回滚流程。
- OpenClaw Fleet 和现有执行能力。
- Hermes Runtime、Profile、队列和工作区能力。
- Agent Router 的路由/调度经验和已有接口。
- V2R1 TenantContext、CanonicalMessage、TenantRouter、MemoryScope、SecretRef、MessageLedger 合同。
- 现有 Gateway、自动领取、任务生命周期回写和测试资产。
- 已有 WebUI、审计和观测代码中可以由证据证明的部分。
### 6.2 必须重构而非重写
- 将 OpenClaw 内部的租户、策略、审批和任务状态决策移到 AIOS;保留执行器和 Fleet 能力。
- 将 Hermes 的控制逻辑移到 AIOS/Router;保留 Runtime、Profile 与 Worker。
- 合并重复 Router/Gateway 入口,形成一个权威 Tenant/Policy middleware。
- 将 SQLite/JSON 原型抽象为 repository/queue/audit 接口,允许先兼容、后替换生产后端。
- 将审计链改为租户或明确审计域分区,并加入签名、外部固化、查询授权和验证作业。
- 将所有 UI 改为 AIOS API 客户端,禁止浏览器直接访问执行层。
- 将测试报告升级为可复现命令、commit、exit code、pass count 和 artifact digest。
### 6.3 暂停继续扩展
在 P0 收敛完成前,不新增:
- 绕过 AIOS Gateway 的 OpenClaw/Hermes 公共入口。
- 未携带 TenantContext 的任务、消息、缓存或后台作业。
- 只存在于个人/临时分支且没有验收合同的大型功能。
- 单次超大提交;每个任务必须可独立审查、测试和回滚。
## 7. 实施路线
### Phase 0:权威化与门禁(立即)
1. 创建 `docs/aios-v2/` 单一事实源和 ADR 索引。
2. 生成能力—提交—测试—部署 Evidence Index。
3. 提取 V2R1 候选能力到窄范围集成分支,不直接合并整个历史分支。
4. 建立统一 TenantContext middleware、Policy Decision 和审计接口。
5. 建立 AIOS Control Plane Gateway 唯一入口与禁止旁路测试。
6. 建立 CI Gate:多租户负向测试、重放、幂等、权限、Web E2E、审计验证。
### Phase 1:执行层解耦
1. OpenClaw Adapter v1:版本化协议、能力发现、start/stop/execute/cancel/status、幂等键、超时、结果回调。
2. Hermes Runtime Adapter v1:enqueue/claim/heartbeat/cancel/result、配额、公平调度和 Tenant Profile 强制。
3. Router 只读取 AIOS 权威注册表与策略结果,不保留第二套租户事实源。
### Phase 2:Web 与生产化
1. AIOS Console:Tenant、Agent、Workflow、Task、Approval、Runtime、Audit、GitOps 页面。
2. 统一 API、RBAC/ABAC、租户会话隔离、CSRF/CORS、速率限制和操作审计。
3. 替换/封装原型数据层,完成迁移、备份恢复、HA、容量和灾备演练。
4. 完成受控基线提升、部署、冒烟和回滚证据。
## 8. 下一阶段出口标准
只有同时满足以下条件,才允许进入 V2R1 集成候选:
- `docs/aios-v2/` 在目标集成分支存在,且架构、ADR、API、租户模型和门禁互相链接。
- AIOS Gateway 是任务创建、审批、状态变更和取消的唯一公共入口。
- OpenClaw/Hermes 旁路调用被代码、网络策略或测试明确拒绝。
- 所有持久化记录和消息均含不可为空的 `tenant_id`,跨租户负向测试通过。
- TenantContext 具备密钥版本、重放缓存、时钟偏差、撤销和审计策略。
- 审计按租户/审计域隔离,能够从 Web/API 授权查询并验证完整性。
- Codex 和 OpenClaw 任务均返回 branch、commit、测试命令、exit code、pass count、风险与回滚说明。
- 变更拆分为可审查提交,不出现无法由 diff 接口检查的单任务超大提交。
- 未执行直接 `main` push、Tag、生产部署或密钥变更。
## 9. 当前状态结论
| 结论 | 状态 |
|---|---|
| 不重写现有代码 | 已决策 |
| 优先复用 OpenClaw/Hermes/Agent Router | 已决策 |
| OpenClaw 与 Hermes 逐步解耦 | 已列入 Phase 1 |
| AIOS 成为唯一控制面 | P0 实施中 |
| 所有功能 Web 化 | P1 规划完成,尚未验收 |
| 所有模块多租户隔离 | 候选合同存在,生产级全链路尚未完成 |
| 所有修改通过 Git 任务流程 | 当前已遵守 |
**总体完成度判定:架构与候选能力约处于“可收敛、不可发布”阶段。下一步不是继续增加功能,而是建立权威事实源、统一入口、租户门禁和可验证的集成闭环。**
@@ -0,0 +1,550 @@
#!/usr/bin/env python3
"""Isolated Git worktree lifecycle for AIOS GitOps write requests.
The long-running controller workspace is read-only and remains detached. Each
write request gets a short-lived detached worktree. A branch-scoped lease and
compare-and-swap ref update prevent concurrent writers from silently replacing
one another.
"""
from __future__ import annotations
import fcntl
import hashlib
import json
import os
import re
import shutil
import subprocess
import threading
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
ZERO_OID = "0" * 40
BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,179}$")
class GitWorkspaceError(RuntimeError):
"""Base error for isolated worktree operations."""
class BranchLeaseBusy(GitWorkspaceError):
"""Another request currently owns the target branch lease."""
class BranchCheckedOut(GitWorkspaceError):
"""The target branch is attached to a non-ephemeral worktree."""
class ConcurrentBranchUpdate(GitWorkspaceError):
"""The branch changed after the request acquired its lease."""
@dataclass
class WorkspaceLease:
branch: str
base: str
path: Path
start_oid: str
local_oid_before: str | None
remote_oid_before: str | None
lock_file: Any
class GitOpsWorktreeManager:
"""Manage one detached worktree per GitOps write request.
A manager instance is safe for a ``ThreadingHTTPServer``. Request state is
stored in thread-local storage, while a filesystem lease serializes writers
targeting the same branch across processes.
"""
def __init__(
self,
controller_workspace: str | Path,
worktree_root: str | Path,
*,
remote: str = "origin",
lock_root: str | Path | None = None,
author_name: str = "chatgpt-gitops-bot",
author_email: str = "chatgpt-gitops-bot@cnnetmkt.top",
) -> None:
self.controller_workspace = Path(controller_workspace).resolve()
self.worktree_root = Path(worktree_root).resolve()
self.remote = remote
self.author_name = author_name
self.author_email = author_email
self._state = threading.local()
if not self.controller_workspace.exists():
raise GitWorkspaceError(
f"controller workspace does not exist: {self.controller_workspace}"
)
common = self._git(
["rev-parse", "--path-format=absolute", "--git-common-dir"],
cwd=self.controller_workspace,
).stdout.strip()
self.common_git_dir = Path(common).resolve()
self.worktree_root.mkdir(parents=True, exist_ok=True)
preferred_lock_root = Path(
lock_root
or os.getenv("AIOS_GITOPS_BRANCH_LOCK_ROOT", "/run/lock/aios-gitops")
)
try:
preferred_lock_root.mkdir(parents=True, exist_ok=True)
self.lock_root = preferred_lock_root.resolve()
except PermissionError:
fallback = self.worktree_root / ".locks"
fallback.mkdir(parents=True, exist_ok=True)
self.lock_root = fallback.resolve()
@staticmethod
def validate_branch(branch: str) -> str:
value = str(branch or "").strip()
invalid = (
not BRANCH_RE.fullmatch(value)
or value in {"main", "master"}
or value.startswith(("main/", "master/"))
or value.endswith(("/", ".", ".lock"))
or value.startswith(("/", "."))
or ".." in value
or "//" in value
or "@{" in value
or "\\" in value
)
if invalid:
raise ValueError(f"invalid or protected branch: {value!r}")
return value
@staticmethod
def validate_path(path: str) -> str:
raw = str(path or "").replace("\\", "/").strip()
candidate = Path(raw)
if (
not raw
or candidate.is_absolute()
or ".." in candidate.parts
or candidate.parts[0] == ".git"
):
raise ValueError(f"invalid repository path: {path!r}")
return str(candidate)
def _git(
self,
args: Iterable[str],
*,
cwd: Path | None = None,
check: bool = True,
) -> subprocess.CompletedProcess[str]:
command = ["git", *list(args)]
proc = subprocess.run(
command,
cwd=str(cwd or self.controller_workspace),
text=True,
capture_output=True,
)
if check and proc.returncode != 0:
message = proc.stderr.strip() or proc.stdout.strip() or "git command failed"
raise GitWorkspaceError(f"{message}; command={command!r}")
return proc
def _git_dir(
self, args: Iterable[str], *, check: bool = True
) -> subprocess.CompletedProcess[str]:
return self._git(
[f"--git-dir={self.common_git_dir}", *list(args)],
cwd=self.controller_workspace,
check=check,
)
def _rev_parse(self, ref: str) -> str | None:
proc = self._git_dir(["rev-parse", "--verify", ref], check=False)
return proc.stdout.strip() if proc.returncode == 0 else None
def _remote_oid(self, branch: str) -> str | None:
proc = self._git(
["ls-remote", "--heads", self.remote, f"refs/heads/{branch}"],
cwd=self.controller_workspace,
check=False,
)
if proc.returncode != 0 or not proc.stdout.strip():
return None
return proc.stdout.split()[0]
def _fetch_remote_tracking(self, branch: str) -> str | None:
remote_oid = self._remote_oid(branch)
if not remote_oid:
return None
destination = f"refs/remotes/{self.remote}/{branch}"
self._git(
[
"fetch",
"--no-tags",
self.remote,
f"+refs/heads/{branch}:{destination}",
],
cwd=self.controller_workspace,
)
return remote_oid
def _checked_out_paths(self, branch: str) -> list[str]:
target_ref = f"refs/heads/{branch}"
proc = self._git_dir(["worktree", "list", "--porcelain"])
paths: list[str] = []
current_path: str | None = None
for line in proc.stdout.splitlines():
if line.startswith("worktree "):
current_path = line.removeprefix("worktree ").strip()
elif line.startswith("branch ") and line.removeprefix("branch ").strip() == target_ref:
if current_path:
paths.append(current_path)
return paths
def _assert_branch_not_checked_out(self, branch: str) -> None:
paths = self._checked_out_paths(branch)
if paths:
raise BranchCheckedOut(
f"branch {branch!r} is checked out at: {', '.join(paths)}"
)
def _acquire_lock(self, branch: str):
digest = hashlib.sha256(branch.encode("utf-8")).hexdigest()
path = self.lock_root / f"{digest}.lock"
handle = path.open("a+", encoding="utf-8")
try:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
handle.close()
raise BranchLeaseBusy(f"branch lease is busy: {branch}") from exc
handle.seek(0)
handle.truncate()
handle.write(json.dumps({"branch": branch, "pid": os.getpid()}))
handle.flush()
return handle
def _release_lock(self, handle: Any) -> None:
try:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
finally:
handle.close()
def _current(self) -> WorkspaceLease | None:
return getattr(self._state, "lease", None)
def current_workspace(self) -> Path:
lease = self._current()
return lease.path if lease else self.controller_workspace
def begin(self, branch: str, base: str = "main", create: bool = False) -> str:
del create # Existing branches are resumed; they are never destructively reset.
if self._current() is not None:
raise GitWorkspaceError("request already owns a worktree lease")
branch = self.validate_branch(branch)
base = str(base or "main").strip()
lock_file = self._acquire_lock(branch)
try:
self._assert_branch_not_checked_out(branch)
remote_oid = self._fetch_remote_tracking(branch)
local_oid = self._rev_parse(f"refs/heads/{branch}")
if remote_oid and local_oid and remote_oid != local_oid:
raise ConcurrentBranchUpdate(
f"local and remote branch diverged before write: {branch}"
)
start_oid = remote_oid or local_oid
if not start_oid:
base_remote = self._fetch_remote_tracking(base)
base_local = self._rev_parse(f"refs/heads/{base}")
start_oid = base_remote or base_local or self._rev_parse(base)
if not start_oid:
raise GitWorkspaceError(f"cannot resolve base ref: {base}")
suffix = uuid.uuid4().hex[:12]
safe_branch = re.sub(r"[^A-Za-z0-9._-]+", "-", branch)[:96]
path = self.worktree_root / f"{safe_branch}-{suffix}"
self._git_dir(["worktree", "add", "--detach", str(path), start_oid])
self._state.lease = WorkspaceLease(
branch=branch,
base=base,
path=path,
start_oid=start_oid,
local_oid_before=local_oid,
remote_oid_before=remote_oid,
lock_file=lock_file,
)
return branch
except Exception:
self._release_lock(lock_file)
raise
def sh(self, args: list[str], check: bool = False) -> dict[str, Any]:
proc = self._git(args, cwd=self.current_workspace(), check=False)
if check and proc.returncode != 0:
message = proc.stderr.strip() or proc.stdout.strip() or "git command failed"
raise GitWorkspaceError(message)
return {
"code": proc.returncode,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
"cmd": ["git", *args],
}
def write_file(self, path: str, content: str) -> str:
lease = self._current()
if lease is None:
raise GitWorkspaceError("write attempted without an active branch lease")
relative = self.validate_path(path)
target = lease.path / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return relative
def write_json(self, path: str, data: Any) -> str:
return self.write_file(
path, json.dumps(data, ensure_ascii=False, indent=2) + "\n"
)
def _update_local_branch(self, lease: WorkspaceLease, new_oid: str) -> None:
self._assert_branch_not_checked_out(lease.branch)
ref = f"refs/heads/{lease.branch}"
expected = lease.local_oid_before or ZERO_OID
proc = self._git_dir(["update-ref", ref, new_oid, expected], check=False)
if proc.returncode != 0:
raise ConcurrentBranchUpdate(
proc.stderr.strip()
or f"local branch changed concurrently: {lease.branch}"
)
def _rollback_local_branch(self, lease: WorkspaceLease, new_oid: str) -> None:
ref = f"refs/heads/{lease.branch}"
if lease.local_oid_before:
self._git_dir(
["update-ref", ref, lease.local_oid_before, new_oid], check=False
)
else:
self._git_dir(["update-ref", "-d", ref, new_oid], check=False)
def _push_remote(self, lease: WorkspaceLease, new_oid: str) -> dict[str, Any]:
remote_url = self._git(
["remote", "get-url", self.remote],
cwd=lease.path,
check=False,
)
if remote_url.returncode != 0:
return {"status": "synced", "target": "common_git_dir"}
command = ["push", "--porcelain"]
if lease.remote_oid_before:
command.append(
f"--force-with-lease=refs/heads/{lease.branch}:{lease.remote_oid_before}"
)
command.extend(
[self.remote, f"HEAD:refs/heads/{lease.branch}"]
)
proc = self._git(command, cwd=lease.path, check=False)
if proc.returncode != 0:
self._rollback_local_branch(lease, new_oid)
raise ConcurrentBranchUpdate(
proc.stderr.strip()
or proc.stdout.strip()
or f"remote branch changed concurrently: {lease.branch}"
)
tracking_ref = f"refs/remotes/{self.remote}/{lease.branch}"
tracking_expected = lease.remote_oid_before or ZERO_OID
self._git_dir(
["update-ref", tracking_ref, new_oid, tracking_expected], check=False
)
return {
"status": "synced",
"target": self.remote,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
}
def commit(self, paths: list[str] | str | None, message: str) -> dict[str, Any]:
lease = self._current()
if lease is None:
raise GitWorkspaceError("commit attempted without an active branch lease")
raw_paths = paths or ["."]
if isinstance(raw_paths, str):
raw_paths = [raw_paths]
safe_paths = [self.validate_path(p) if p != "." else "." for p in raw_paths]
self._git(["add", "--", *safe_paths], cwd=lease.path)
status = self._git(["status", "--porcelain"], cwd=lease.path).stdout.strip()
if not status:
result = {
"status": "no_changes",
"branch": lease.branch,
"rev": lease.start_oid[:12],
"sync": {"status": "unchanged"},
"workspace_mode": "detached",
}
self.finish(success=True)
return result
commit_proc = self._git(
[
"-c",
f"user.name={self.author_name}",
"-c",
f"user.email={self.author_email}",
"commit",
"-m",
message,
],
cwd=lease.path,
)
new_oid = self._git(["rev-parse", "HEAD"], cwd=lease.path).stdout.strip()
self._update_local_branch(lease, new_oid)
sync = self._push_remote(lease, new_oid)
result = {
"status": "committed",
"branch": lease.branch,
"rev": new_oid[:12],
"commit": new_oid,
"git": {
"code": commit_proc.returncode,
"stdout": commit_proc.stdout.strip(),
"stderr": commit_proc.stderr.strip(),
},
"sync": sync,
"workspace_mode": "detached",
}
self.finish(success=True)
return result
def finish(self, *, success: bool) -> dict[str, Any] | None:
lease = self._current()
if lease is None:
return None
self._state.lease = None
retained = False
cleanup_error: str | None = None
try:
status = self._git(
["status", "--porcelain"], cwd=lease.path, check=False
).stdout.strip()
if success and not status:
proc = self._git_dir(
["worktree", "remove", str(lease.path)], check=False
)
if proc.returncode != 0:
cleanup_error = proc.stderr.strip() or proc.stdout.strip()
retained = True
else:
retained = lease.path.exists()
finally:
self._release_lock(lease.lock_file)
self._git_dir(["worktree", "prune", "--expire", "now"], check=False)
return {
"branch": lease.branch,
"worktree": str(lease.path),
"retained": retained,
"cleanup_error": cleanup_error,
}
def abort_current(self) -> dict[str, Any] | None:
"""Release an unfinished request, retaining dirty evidence for review."""
lease = self._current()
if lease is None:
return None
status = self._git(
["status", "--porcelain"], cwd=lease.path, check=False
).stdout.strip()
return self.finish(success=not bool(status))
def sync_branch(self, branch: str) -> dict[str, Any]:
"""Compatibility hook for the legacy API entrypoint."""
return {
"status": "managed_by_isolated_worktree",
"branch": self.validate_branch(branch),
}
def sync_tag(self, tag: str) -> dict[str, Any]:
proc = self._git(
["push", self.remote, f"refs/tags/{tag}:refs/tags/{tag}"],
cwd=self.controller_workspace,
check=False,
)
if proc.returncode != 0:
raise GitWorkspaceError(proc.stderr.strip() or proc.stdout.strip())
return {
"status": "synced",
"target": self.remote,
"tag": tag,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
}
def runtime_status(self) -> dict[str, Any]:
branch = self._git(
["branch", "--show-current"], cwd=self.controller_workspace
).stdout.strip()
head = self._git(
["rev-parse", "HEAD"], cwd=self.controller_workspace
).stdout.strip()
dirty = bool(
self._git(
["status", "--porcelain"], cwd=self.controller_workspace
).stdout.strip()
)
return {
"workspace_mode": "branch" if branch else "detached",
"workspace_branch": branch or None,
"workspace_ref": head,
"workspace_dirty": dirty,
"common_git_dir": str(self.common_git_dir),
"task_worktree_root": str(self.worktree_root),
}
def remove_abandoned_clean_worktrees(
manager: GitOpsWorktreeManager, *, prefix: str | Path | None = None
) -> list[str]:
"""Remove clean detached worktrees below the managed root.
Dirty worktrees are deliberately retained. This helper is intended for a
startup recovery job and never removes worktrees outside the configured
managed root.
"""
root = Path(prefix or manager.worktree_root).resolve()
removed: list[str] = []
proc = manager._git_dir(["worktree", "list", "--porcelain"])
candidates: list[Path] = []
current: Path | None = None
detached = False
for line in [*proc.stdout.splitlines(), ""]:
if line.startswith("worktree "):
if current and detached:
candidates.append(current)
current = Path(line.removeprefix("worktree ").strip()).resolve()
detached = False
elif line == "detached":
detached = True
elif not line and current and detached:
candidates.append(current)
current = None
detached = False
for path in candidates:
if path == manager.controller_workspace or root not in path.parents:
continue
status = manager._git(["status", "--porcelain"], cwd=path, check=False)
if status.returncode == 0 and not status.stdout.strip():
removal = manager._git_dir(
["worktree", "remove", str(path)], check=False
)
if removal.returncode == 0:
removed.append(str(path))
elif not path.exists():
shutil.rmtree(path, ignore_errors=True)
manager._git_dir(["worktree", "prune", "--expire", "now"], check=False)
return removed
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/aios-v2-implementation-status-next-phase-20260720",
"base": "main",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-19T16:21:00Z",
"direct_main_push": false,
"owner_approval_required": true
}