diff --git a/loopx/control_plane/coordination/claim_execution.ts b/loopx/control_plane/coordination/claim_execution.ts index f357573d18..27b9857869 100644 --- a/loopx/control_plane/coordination/claim_execution.ts +++ b/loopx/control_plane/coordination/claim_execution.ts @@ -8,7 +8,7 @@ import {indexCoordinationProjection} from "./coordination_projection.ts"; import {canonicalTaskLease, canonicalTaskLeaseAcquireFacts} from "./task_lease_state.ts"; import {coordinationTodoWriteScopes} from "./todo_write_scopes.ts"; import {leaseEpoch, leaseVersion, leaseIsActive, normalizeTtl, type LeaseRecord} from "../work_items/task_lease_acquire.ts"; -import {sameLeaseWorkspace} from "../work_items/task_lease_workspace.ts"; +import {sameLeaseWorkspace, type LeaseWorkspace} from "../work_items/task_lease_workspace.ts"; import {leaseWriteRepository} from "../work_items/task_lease_repository.ts"; import {evaluateTaskLeaseAcquireDecision, materializeTaskLeaseAcquire} from "../work_items/task_lease_acquire_decision.ts"; import {decideTaskLeaseLifecycle, materializeTaskLeaseLifecycle} from "../work_items/task_lease_lifecycle_decision.ts"; @@ -24,6 +24,8 @@ export function claimExecutionReference(id: unknown, key: unknown, version: unkn type ExecutionInput = {goal_id: string; todo_id: string; actor_agent_id: string | null; registered_agents: readonly string[]; now: Date}; +export type ClaimWorkspaceObserver = (writeScopes: readonly string[]) => Promise; + /** Resolve only this accepted execution. No acquisition, adoption of a newer * generation, or write retry is hidden behind a progress/completion request. */ export async function claimExecutionProof(store: AuthorityStore, head: JsonObject, @@ -82,8 +84,9 @@ export function maintainClaimExecution(head: JsonObject, input: ExecutionInput, /** Prepare execution only after the Todo claim owner accepts the actor (and any * transfer grant). Expired execution is reclaimable without the old process; * acquisition still validates scope, eligibility and generation under one CAS. */ -export function prepareClaimExecution(head: JsonObject, - input: ExecutionInput & {operation_id: string; claimed_by: string}): {lease: LeaseRecord; changed: boolean} { +export async function prepareClaimExecution(head: JsonObject, + input: ExecutionInput & {operation_id: string; claimed_by: string}, + observeWorkspace?: ClaimWorkspaceObserver): Promise<{lease: LeaseRecord; changed: boolean; overlapAdvisories?: JsonObject[]}> { const index = indexCoordinationProjection(head, input.goal_id); const facts = canonicalTaskLeaseAcquireFacts(index, input.goal_id, input.todo_id, input.registered_agents, input.now); const current = facts.current; @@ -92,16 +95,26 @@ export function prepareClaimExecution(head: JsonObject, const renewed = maintainClaimExecution(head, input, current); return {lease: renewed ?? current, changed: renewed !== null}; } + const todo = index.todos.get(input.todo_id)!; + const writeScopes = coordinationTodoWriteScopes(todo); + // Only declared repository code work can use the code-edit isolation mode. + // Shared-state and historical callers retain their conservative exclusion. + const workspace = observeWorkspace && todo.task_domain === "code" && + typeof todo.task_repository === "string" && writeScopes.length > 0 + ? await observeWorkspace(writeScopes) : null; const command = {owner: input.claimed_by, idempotency_key: `claim-${canonicalAuthoritySha256({goal_id: input.goal_id, todo_id: input.todo_id, owner: input.claimed_by, operation_id: input.operation_id})}`, expected_version: current ? leaseVersion(current) : 0, ttl_seconds: normalizeTtl(null), - write_scopes: coordinationTodoWriteScopes(index.todos.get(input.todo_id)!)}; + write_scopes: writeScopes, ...(workspace ? {write_workspace: workspace} : {})}; const decision = evaluateTaskLeaseAcquireDecision({handoff_mode: String(head.handoff_mode), registered_agents: [...input.registered_agents], ...facts, // Claim admission has already authorized this assignment. Acquire and the // Todo owner change commit together; the old assignment is not a new gate. todo: facts.todo === null ? null : {...facts.todo, claimed_by: input.claimed_by}, command}); if (decision.outcome !== "apply") throw new AuthorityStoreProtocolError(`Task claim execution rejected: ${decision.code}`); - return {lease: materializeTaskLeaseAcquire(input, command, decision, input.now), changed: true}; + const overlapAdvisories = (decision.overlap_advisory_indexes ?? []).map(index => ({ + todo_id: facts.other_leases[index].todo_id, owner: facts.other_leases[index].owner ?? null, + write_scopes: facts.other_leases[index].write_scopes, reason: "independent_worktree_integration_overlap"})); + return {lease: materializeTaskLeaseAcquire(input, command, decision, input.now), changed: true, overlapAdvisories}; } diff --git a/loopx/control_plane/coordination/local_authority.py b/loopx/control_plane/coordination/local_authority.py index 883489bc13..485b5f4d50 100644 --- a/loopx/control_plane/coordination/local_authority.py +++ b/loopx/control_plane/coordination/local_authority.py @@ -14,6 +14,7 @@ from uuid import uuid4 from ...agent_registry import registered_agent_ids_from_registry +from ..agents.workspace_guard import _git_worktree_root from .authority_source_capture import authority_registry_source from ..runtime.time import now_local_iso as now_local from ..effect_runtime import ( @@ -161,6 +162,11 @@ def claim_canonical_todo_if_promoted( """Route a claim to the canonical TypeScript transaction owner.""" with authority_registry_source(registry_path) as registry_source: registered = registered_agent_ids_from_registry(registry_path, goal_id) + # This is only a candidate location. The lease owner observes physical + # isolation and the current canonical scopes before granting execution. + worktree = _git_worktree_root(Path.cwd()) if manage_execution else None + if worktree is not None and not (worktree / ".git").is_file(): + worktree = None result = effect_runtime_result( LOCAL_COORDINATION_TODO_CLAIM_METHOD, { @@ -180,6 +186,7 @@ def claim_canonical_todo_if_promoted( else f"todo-claim:{goal_id}:{todo_id}:{uuid4().hex}" ), **({"manage_execution": True} if manage_execution else {}), + **({"write_worktree": str(worktree)} if worktree is not None else {}), "lease_request": ( { "idempotency_key": task_lease_idempotency_key, diff --git a/loopx/control_plane/coordination/local_authority_runtime.ts b/loopx/control_plane/coordination/local_authority_runtime.ts index 17dee78cc5..87bfdff9bf 100644 --- a/loopx/control_plane/coordination/local_authority_runtime.ts +++ b/loopx/control_plane/coordination/local_authority_runtime.ts @@ -5,6 +5,8 @@ import {reviewedPromotionPlan, promotionPlanDigest, decodeReviewedPromotionOpera import {registryAuthoritySourceCheck} from "./authority_source.ts"; import {ResearchTerminalEvidenceHost} from "../capabilities/explore_research_terminal.ts"; import {decodeTaskLeaseProof} from "./task_lease_proof.ts"; +import {LeaseWorktreeObservationError, observeLeaseWorktree} from "../work_items/task_lease_workspace.ts"; +import {evaluateTaskLeaseWriteScopesOverlap} from "../work_items/task_lease_acquire_decision.ts"; import {COORDINATION_TODO_ARCHIVE_RESULT_SCHEMA} from "./todo_archive.ts"; import {readCoordinationOwnership} from "./ownership_observation.ts"; import {executeTodoContinuation} from "./todo_continuation.ts"; @@ -1035,6 +1037,11 @@ export async function claimLocalCoordinationTodo( } if (Object.hasOwn(input, "manage_execution") && input.schema_version !== LOCAL_COORDINATION_TODO_MANAGED_CLAIM_REQUEST_SCHEMA) throw new TypeError("Managed execution requires claim request v2"); + const worktree = input.write_worktree; + if (worktree !== undefined && (input.schema_version !== LOCAL_COORDINATION_TODO_MANAGED_CLAIM_REQUEST_SCHEMA || + input.manage_execution !== true || typeof worktree !== "string" || !isAbsolute(worktree))) { + throw new TypeError("Worktree observation requires a managed claim and an absolute caller path"); + } const authoritySourcesCurrent = registryAuthoritySourceCheck(input, input.schema_version !== LOCAL_COORDINATION_TODO_CLAIM_REQUEST_SCHEMA); if (typeof input.dry_run !== "boolean") { @@ -1085,7 +1092,15 @@ export async function claimLocalCoordinationTodo( ...(input.manage_execution === undefined ? {} : {manage_execution: input.manage_execution as boolean}), dry_run: input.dry_run === true, now: claimObservedAt(input.observed_at), - }, authoritySourcesCurrent); + }, authoritySourcesCurrent, worktree === undefined ? undefined : async writeScopes => { + try { + return await observeLeaseWorktree(worktree as string, path => + evaluateTaskLeaseWriteScopesOverlap({left: [...writeScopes], right: [path]}).overlap === true); + } catch (error) { + throw new Error(error instanceof LeaseWorktreeObservationError ? error.message + : "Cannot verify independent Git worktree and origin for task claim"); + } + }); return { ...result, source_authority: sourceAuthority, diff --git a/loopx/control_plane/coordination/todo_claim.ts b/loopx/control_plane/coordination/todo_claim.ts index b39c3d5ed0..f148052022 100644 --- a/loopx/control_plane/coordination/todo_claim.ts +++ b/loopx/control_plane/coordination/todo_claim.ts @@ -1,4 +1,4 @@ -import {prepareClaimExecution} from "./claim_execution.ts"; +import {prepareClaimExecution, type ClaimWorkspaceObserver} from "./claim_execution.ts"; import {retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts"; import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts"; import {currentLeaseAcquisitionProof} from "./lease_acquisition_proof.ts"; @@ -340,13 +340,14 @@ export async function executeCoordinationTodoClaim( store: AuthorityStore, rawInput: CoordinationTodoClaimInput, authoritySourcesCurrent: AuthoritySourceCheck = uncheckedAuthoritySource, + observeWorkspace?: ClaimWorkspaceObserver, ): Promise { // Retry only a conclusive provider CAS rejection. Each attempt rereads the // original receipt and the complete head, then rechecks source authorization, // claim ownership, acceptance and lease/write-scope exclusion. Pinned revisions // (including handoff grants) must return to their caller for a fresh observation. for (let attempt = 0; ; attempt++) { - const result = await executeClaimAttempt(store, rawInput, authoritySourcesCurrent); + const result = await executeClaimAttempt(store, rawInput, authoritySourcesCurrent, observeWorkspace); if (attempt >= 2 || rawInput.expected_provider_revision !== undefined || rawInput.transfer_grant !== undefined || result.status !== "conflict" || result.conflict_kind !== "provider_revision_mismatch") return result; @@ -357,6 +358,7 @@ async function executeClaimAttempt( store: AuthorityStore, rawInput: CoordinationTodoClaimInput, authoritySourcesCurrent: AuthoritySourceCheck, + observeWorkspace?: ClaimWorkspaceObserver, ): Promise { let input: CoordinationTodoClaimInput; try { @@ -583,13 +585,15 @@ async function executeClaimAttempt( let lease: JsonObject | null = null; let leaseChanged = false; let leaseIdempotent = false; + let overlapAdvisories: JsonObject[] = []; try { const currentLease = projection.leases.get(input.todo_id); if (handoffMode === "hard_lease" && input.manage_execution === true) { - const prepared = prepareClaimExecution(head.head, input); + const prepared = await prepareClaimExecution(head.head, input, observeWorkspace); lease = prepared.lease; leaseChanged = prepared.changed; leaseIdempotent = !leaseChanged; + overlapAdvisories = prepared.overlapAdvisories ?? []; } else if (handoffMode === "hard_lease" && leaseRequest !== null) { // Validate required scopes before planning; a caller cannot omit a required conflict. const writeScopes = coordinationTodoWriteScopes(todo); @@ -692,6 +696,7 @@ async function executeClaimAttempt( handoff_mode: handoffMode, ...(changed ? {updated_at: updatedAt} : {}), mutation_authority: mutationAuthority, + ...(overlapAdvisories.length ? {integration_overlap_advisories: overlapAdvisories} : {}), ...(input.manage_execution ? {execution_managed: true, claim_operation_id: input.operation_id} : {}), ...(leaseRequest === null && !(input.manage_execution && handoffMode === "hard_lease") ? {} : { todo_changed: todoChanged, diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 49a3405df2..7004999b59 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -121,6 +121,7 @@ teaches a reusable control-plane lesson. | `content_layout_sequence_acceptance_gap` | A carousel passes independent per-page density checks, but the cover is not the density peak or interior pages remain visibly sparse. | Typed layout plan, selected template `page_sequence`, renderer measurement packet, per-page densities, and `layout-check` failures. | Acceptance modeled each page in isolation and omitted sequence-level cover prominence and interior compactness defaults. | Keep project-specific narrative rules in the writing adapter, but make built-in templates require a cover first, a first-page density maximum, and an interior density floor; lock low-interior and denser-than-cover cases with focused negative smokes. | | `shared_adapter_local_path_leak` | A shared adapter projection, such as Lark Kanban sync, would write an absolute local checkout, runtime, active-state, raw-log, or artifact path into a remote/shared row. | adapter dry-run payload, generated record values, docs boundary section, public/private scan. | Adapter reused local executor context as shared control-plane metadata. | Strip or replace the local path at the adapter projection layer, add a focused smoke that rejects the path, and only store explicit worker launch context when the row was authored for that purpose. | | `peer_workspace_boundary_gap` | A peer edits or prepares repository changes from a non-git directory, unrelated worktree, or checkout that violates the selected task/repository isolation policy; a cross-repository repair may also be misclassified against the goal repo. | `agent_identity`, selected todo `task_repository` and write scopes, goal `repo`, current git origin/worktree/root, quota payload, heartbeat prompt commands, AGENTS worktree policy. | Workspace safety was inferred from identity labels, prompt text, or the goal repo alone instead of the selected task and `quota should-run`. | Add or repair `agent_workspace_guard_v1`; use a credential-free first-class `task_repository` only for genuine cross-repository tasks, and require a matching linked worktree. Block delivery and spend until the peer moves to a compliant worktree and reruns the guard with the same `--agent-id`. Keep the goal repo fallback and do not treat the field as scope or write authority. Read-only and monitor-only work should not be blocked merely by identity. | +| `managed_claim_worktree_observation_gap` | Ordinary `todo claim` reports `write_scope_conflict` for code tasks in independent sibling Git worktrees, while explicit `task-lease acquire --write-worktree` admits the same declared scopes. | Current task domain, repository and required scopes; actual Git roots; original claim response; exact lease owner, key and version; matched File/SQLite CLI reproduction and physical-checkout refusal. | Managed acquisition omitted the existing code-edit workspace observation and treated repository-relative code scopes as shared writes. | Pass only a candidate caller root to the existing lease observer; observe current canonical code scopes, repository and physical paths before the typed acquisition decision. Preserve same-checkout, redirected-path and foreign-repository refusals, shared-state exclusion, integration advisories and original-lease replay. For an older installed caller, use normal explicit acquire with all current scopes, its own worktree and current CAS, then an ordinary managed claim; do not turn that existing key into an inline reacquire without matching observation. Recover ambiguous original receipts first, never take over another lease or infer permission from workspace labels. | | `workspace_guard_action_selection_projection_gap` | A foreign default Todo hides eligible alternatives, or an explicit choice repeatedly reports `control_repair` while unselected reentry restores a different default. | Initial and explicit selection qualification, Todo priorities and ready deferred successors, workspace guard, action portfolio, and receipt identity. | Workspace repair may hide candidate selection; alternatively, a refused explicit choice leaves no selected Todo and the legacy guard fallback overwrites the typed refusal with a repair for the default. | Preserve alternative selection when otherwise eligible. If selection is already deferred or rejected with no selected Todo, preserve that refusal without guarding the default. Separate hard authority/resume/lease gates from priority advice. A ready higher-priority deferred successor must not veto an otherwise eligible explicit choice. Keep the default recommendation and planning context, repair a priority-only refusal, then revalidate the chosen workspace before binding. An unbound choice is not protected by execution identity. | | `boundary_repair_action_selection_projection_gap` | Quota selects a Todo whose required write scopes are absent from the current Goal boundary, emits `boundary_projection_repair`, and binds that blocked Todo even though another eligible Todo is runnable; same-turn selection of the alternative then fails with a receipt identity conflict. | Initial and explicitly selected `quota should-run` packets, Goal write scope, selected Todo write scopes, action portfolio, interaction selection command, and append-only heartbeat receipt events. | Boundary repair correctly blocked the default delivery, but the planning gate treated self-repair as if no eligible alternative could be selected, so recommendation order became hidden settlement authority. | Keep boundary repair fail-closed for the mismatched Todo while projecting the typed action portfolio when alternatives exist. Commit the first guard identity-less, revalidate an explicit choice against the live boundary, and append one deterministic identity upgrade without rewriting the original receipt. Retain hard conflicts for genuinely bound settlements and cover the full CLI round trip. | | `monitor_successor_routing_gap` | A material monitor poll creates a natural-language `advance` successor for another repository, but the child has no repository, action, continuation, capabilities, stable target, or creation receipt; quota then routes plausible work without a safe workspace contract. | Parent monitor metadata, `quota monitor-poll` argv and artifact, child Todo metadata, rollout event successor ids, selected Todo workspace guard. | The agent extended a bounded observation into standing work, while the monitor-poll successor API accepted only text and an optional claimer and discarded typed routing. | Retire the unintended monitor/vision. Require explicit action semantics for every monitor-generated agent successor and explicit successor repository when the source monitor is repository-bound; carry optional execution capabilities, default continuation to `independent_handoff`, and derive a stable transition target when none is supplied. Record compact successor ids and routing receipts in the monitor artifact and rollout event. External-repository work must re-enter through that typed Todo rather than prior observation prose. | diff --git a/tests/control_plane_ts/managed_claim_execution_conformance.ts b/tests/control_plane_ts/managed_claim_execution_conformance.ts index c94e1e8486..ca52ec2857 100644 --- a/tests/control_plane_ts/managed_claim_execution_conformance.ts +++ b/tests/control_plane_ts/managed_claim_execution_conformance.ts @@ -46,6 +46,47 @@ export function registerManagedClaimExecutionConformance(provider: string, facto clear_claim: false, validation_declaration: null, validation_receipt: null, completion_policy_request: null, dry_run: false, now: request.now}; } + test(`${provider} managed execution: worktree observation uses canonical code scopes only for new execution`, async t => { + const {store, request} = await setup(t); + const before = await loaded(store); + const todo = (before.head.todos as JsonObject[])[0]!; + const repository = "git:github.com/example/project"; + assert.equal((await store.commitAuthority(prepareCoordinationProjectionCommit({goal_id: request.goal_id, + operation_id: "declare-code", expected_provider_revision: before.provider_revision, projection: before.head, + mutations: [{kind: "todo_upsert", todo: {...todo, task_domain: "code", task_repository: repository}}]}))).status, "applied"); + const workspace = {host: "1".repeat(64), common_directory: "2".repeat(64), worktree: "3".repeat(64), repository}; + const observed: string[][] = []; + const accepted = await claim(store, request, undefined, async scopes => { + observed.push([...scopes]); return workspace; + }); + assert.equal(accepted.status, "applied", JSON.stringify(accepted)); + assert.deepEqual(observed, [["src/**"]]); + assert.deepEqual((accepted.lease as JsonObject).write_workspace, workspace); + const current = await loaded(store); + const unexpected = async () => {throw new Error("Recovery must retain the accepted execution, not acquire another workspace");}; + assert.equal((await claim(store, request, undefined, unexpected)).status, "replayed"); + assert.deepEqual(await loaded(store), current); + const continuation = await claim(store, {...request, operation_id: "continue-current-execution"}, undefined, unexpected); + assert.equal(continuation.status, "no_change"); + assert.deepEqual((continuation.lease as JsonObject).write_workspace, workspace); + }); + test(`${provider} managed execution: a failed worktree observation cannot grant or assign work`, async t => { + const {store, request} = await setup(t); + const before = await loaded(store); + const todo = (before.head.todos as JsonObject[])[0]!; + assert.equal((await store.commitAuthority(prepareCoordinationProjectionCommit({goal_id: request.goal_id, + operation_id: "declare-code", expected_provider_revision: before.provider_revision, projection: before.head, + mutations: [{kind: "todo_upsert", todo: {...todo, task_domain: "code", task_repository: "git:github.com/example/project"}}]}))).status, "applied"); + const declared = await loaded(store); + const rejected = await claim(store, request, undefined, async () => {throw new Error("Physical isolation unavailable");}); + assert.equal(rejected.reason_code, "invalid_coordination_task_lease"); + assert.deepEqual(await loaded(store), declared); + assert.equal((await store.readReceipt(request.operation_id)).status, "missing"); + const legacy = await setup(t); + const unchanged = await claim(legacy.store, legacy.request, undefined, async () => {throw new Error("Non-code work must keep shared-state exclusion");}); + assert.equal(unchanged.status, "applied"); + assert.equal((unchanged.lease as JsonObject).write_workspace, undefined); + }); test(`${provider} managed execution: terminal recovery uses ordinary claim admission without effects`, async t => { for (const active of [false, true]) { const {store, request} = await setup(t); diff --git a/tests/test_task_lease_worktree.py b/tests/test_task_lease_worktree.py index 9e5ee21fff..28e65c4a27 100644 --- a/tests/test_task_lease_worktree.py +++ b/tests/test_task_lease_worktree.py @@ -116,3 +116,83 @@ def acquire(key, path, expected=0, scope="src/**"): assert released["released"] assert cli("inspect", "d")["lease"] == legacy["lease"] assert not state.exists() + + +@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="worktree fixture requires symlinks") +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_ordinary_claim_uses_its_independent_worktree(tmp_path, monkeypatch, capsys, provider): + """Normal claims admit sibling edits without weakening physical collisions.""" + isolate_sqlite_runtime(tmp_path, monkeypatch) + project = tmp_path / "repo" + project.mkdir() + + def git(*args): + return subprocess.run(["git", "-C", str(project), *args], capture_output=True, text=True, check=True).stdout.strip() + + git("init") + git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.com", "commit", "--allow-empty", "-m", "fixture") + git("remote", "add", "origin", "https://github.com/example/project.git") + a, b = tmp_path / "a", tmp_path / "b" + git("worktree", "add", "-b", "a", str(a)) + git("worktree", "add", "-b", "b", str(b)) + alias = tmp_path / "alias" + alias.symlink_to(a, target_is_directory=True) + runtime, state, registry = tmp_path / "runtime", tmp_path / "state.md", tmp_path / "registry.json" + goal = "ordinary-worktree-claim" + state.write_text("# Ordinary claim\n\n## Agent Todo\n") + registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ + "id": goal, "repo": str(project), "state_file": str(state), + "coordination": {"registered_agents": ["agent-a", "agent-b"]}, + }]})) + projection = build_todo_runtime_shadow_projection(goal_id=goal, handoff_mode="hard_lease", leases=[], todos=[{ + "schema_version": "todo_item_v0", "todo_id": f"todo_ordinary_{key}", "role": "agent", "status": "open", "done": False, + "text": "Edit isolated source", "archive_state": "active", "source_section": "Agent Todo", "index": i, + "task_class": "advancement_task", "task_domain": "code", "task_repository": "git:github.com/example/project", + "required_write_scopes": ["src/**"], + } for i, key in enumerate(("a", "b"), 1)]) + initialize_canonical_authority(runtime, goal, projection, state_path=state, provider=provider) + + def cli(command, key, *args, expected=0): + rc = cli_main(["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", + *command, "--goal-id", goal, "--todo-id", f"todo_ordinary_{key}", *args]) + output = capsys.readouterr().out + assert rc == expected, output + return json.loads(output) + + def claim(key, operation, expected=0): + return cli(["todo", "claim"], key, "--agent-id", f"agent-{key}", "--claimed-by", f"agent-{key}", + "--claim-operation-id", operation, expected=expected) + + monkeypatch.chdir(a) + assert claim("a", "claim-a")["execution_managed"] + first = cli(["task-lease", "inspect"], "a")["lease"] + monkeypatch.chdir(alias) + assert claim("b", "same-checkout", expected=1)["ok"] is False + assert not cli(["task-lease", "inspect"], "b")["active"] + monkeypatch.chdir(b) + (b / "src").symlink_to(a, target_is_directory=True) + assert claim("b", "redirected-scope", expected=1)["ok"] is False + (b / "src").unlink() + git("remote", "set-url", "origin", "https://github.com/example/other.git") + assert claim("b", "wrong-repository", expected=1)["ok"] is False + git("remote", "set-url", "origin", "https://github.com/example/project.git") + (b / "nested").mkdir() + monkeypatch.chdir(b / "nested") + accepted = claim("b", "claim-b") + assert accepted["execution_managed"] + assert accepted["integration_overlap_advisories"] == [{ + "todo_id": "todo_ordinary_a", "owner": "agent-a", "write_scopes": ["src/**"], + "reason": "independent_worktree_integration_overlap", + }] + second = cli(["task-lease", "inspect"], "b")["lease"] + assert "write_workspace" in first + assert first["write_workspace"] != second["write_workspace"] + assert str(tmp_path) not in json.dumps(second) + assert cli(["todo", "list"], "b")["todo"]["claimed_by"] == "agent-b" + replay = claim("b", "claim-b") + assert replay["status"] == "replayed" + assert cli(["task-lease", "inspect"], "b")["lease"] == second + cli(["task-lease", "release"], "b", "--owner", "agent-b", "--idempotency-key", second["idempotency_key"], + "--expected-version", str(second["version"])) + assert claim("b", "claim-b", expected=1)["ok"] is False + assert not cli(["task-lease", "inspect"], "b")["active"]