Skip to content

Commit 03794d7

Browse files
authored
Merge pull request #5676 from loopx-project/codex/local-goal-write-admission-20261005
fix(quota): reuse local Goal defaults and scope matching
2 parents 23935d5 + ababc82 commit 03794d7

9 files changed

Lines changed: 368 additions & 13 deletions

‎docs/quota-allocation.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1489,12 +1489,24 @@ Post-turn accounting protocol:
14891489
registered project root. This lets validated non-repository work settle
14901490
without inventing a repository, including peer research and material work.
14911491
The existing Todo claim/lease and completion validator still apply; local
1492-
delivery is not `same_agent_non_delivery`. A Git peer delivery still requires
1492+
identity does not waive causal delivery requirements. A Git peer delivery still requires
14931493
an `independent_git_worktree`. An explicit Git task repository or an explicit
14941494
owner isolation requirement rejects a local Goal receipt. An outside-root
14951495
workspace cannot produce that local receipt. For originless Git, the checkout
14961496
root must equal the registered project root; nested repositories, linked
14971497
worktrees, invalid/empty origins and failed Git config reads cannot fall back.
1498+
- `quota should-run` reuses that registered `local_goal` identity for local
1499+
tasks by default. Declaring write scopes does not turn local work into a Git
1500+
edit or require a particular task domain or continuation marker. An explicit
1501+
task repository and owner isolation requirements keep their existing guards.
1502+
Relative Goal scopes keep their existing matching semantics; absolute grants
1503+
under the registered root are projected into the same relative view, including
1504+
existing glob patterns. The existing boundary guard alone checks coverage.
1505+
Caller cwd does not rebase the declared targets or itself block local work.
1506+
If actual delivery is produced in the Goal project from another cwd, use
1507+
`refresh-state --delivery-workspace-path <Goal-project>`; settlement consumes
1508+
that recorded local workspace without requiring a cwd move. This changes local
1509+
task admission, not grants, claim/lease or causal settlement requirements.
14981510
- `todo complete --evidence <pointer>` can record a validated local artifact.
14991511
`--result-file` additionally requires approved Goal acceptance criteria bound
15001512
to that Todo. A standalone Todo validator does not establish Goal acceptance;

‎loopx/control_plane/agents/workspace_guard.py‎

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,13 @@ def _peer_work_requires_isolated_workspace(
323323
agent_todo_summary: dict[str, Any] | None,
324324
*,
325325
selected_todo: dict[str, Any] | None = None,
326+
local_goal_workspace: bool = False,
326327
) -> bool:
327328
explicit = workspace_guard_policy.get("peer_independent_worktree_required")
328329
if explicit is not None:
329330
return explicit is True
331+
if local_goal_workspace:
332+
return False
330333
candidate = (
331334
selected_todo
332335
if isinstance(selected_todo, dict) and selected_todo
@@ -344,13 +347,47 @@ def _peer_work_requires_isolated_workspace(
344347
)
345348

346349

350+
def observe_goal_local_workspace(
351+
goal: dict[str, Any],
352+
selected_todo: dict[str, Any] | None,
353+
allowed_write_scopes: list[str] | None = None,
354+
) -> dict[str, Any]:
355+
"""Reuse the registered workspace identity and project its existing grants."""
356+
empty: dict[str, Any] = {}
357+
if not selected_todo or selected_todo.get("task_repository"):
358+
return empty
359+
repo = goal.get("repo") or goal.get("project") or goal.get("root")
360+
goal_id = goal.get("goal_id") or goal.get("id")
361+
if not repo or not goal_id:
362+
return empty
363+
root = Path(str(repo)).expanduser()
364+
if not root.is_absolute():
365+
return empty
366+
snapshot = capture_delivery_workspace(
367+
root, local_goal_id=str(goal_id), local_project_root=root
368+
)
369+
if not snapshot or snapshot.get("identity_kind") != "local_goal":
370+
return empty
371+
boundary = goal.get("coordination") or {}
372+
raw_scopes = boundary.get("write_scope") if isinstance(boundary, dict) else None
373+
scopes = allowed_write_scopes if allowed_write_scopes is not None else raw_scopes
374+
if not isinstance(scopes, list) or any(not isinstance(scope, str) for scope in scopes):
375+
return empty
376+
from ..quota.settlement_workspace_causality import project_goal_write_scopes
377+
378+
# Physical identity is resolved above. Scope authority stays relative to the
379+
# registered spelling, including an explicit symlink alias of that root.
380+
return {"workspace": snapshot, **project_goal_write_scopes(str(root), scopes)}
381+
382+
347383
def build_agent_workspace_guard(
348384
goal: dict[str, Any],
349385
agent_identity: dict[str, Any] | None,
350386
*,
351387
agent_todo_summary: dict[str, Any] | None = None,
352388
selected_todo: dict[str, Any] | None = None,
353389
current_path: Path | None = None,
390+
local_workspace: dict[str, Any] | None = None,
354391
) -> dict[str, Any] | None:
355392
if not isinstance(agent_identity, dict):
356393
return None
@@ -361,18 +398,27 @@ def build_agent_workspace_guard(
361398
)
362399
if len(agent_identity.get("registered_agents") or []) <= 1:
363400
return None
364-
if not _peer_work_requires_isolated_workspace(
365-
workspace_guard_policy,
366-
agent_todo_summary,
367-
selected_todo=selected_todo,
368-
):
369-
return None
370401
current_path = current_path or Path.cwd()
371402
candidate = (
372403
selected_todo
373404
if isinstance(selected_todo, dict) and selected_todo
374405
else next(iter(_peer_candidate_items(agent_todo_summary)), {})
375406
)
407+
local = (
408+
local_workspace
409+
if local_workspace is not None
410+
else observe_goal_local_workspace(goal, candidate)
411+
)
412+
# Local declarations are relative to the registered Goal target, not the
413+
# caller cwd. Causal accounting still names the actual delivery workspace.
414+
local_admitted = (local.get("workspace") or {}).get("identity_kind") == "local_goal"
415+
if not _peer_work_requires_isolated_workspace(
416+
workspace_guard_policy,
417+
agent_todo_summary,
418+
selected_todo=selected_todo,
419+
local_goal_workspace=local_admitted,
420+
):
421+
return None
376422
task_repository = normalize_todo_task_repository(candidate.get("task_repository"))
377423
current_workspace = ""
378424
repository_source = "goal.repo"

‎loopx/control_plane/quota/settlement_workspace_causality.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@
3232
DELIVERY_WORKSPACE_REQUIREMENTS = frozenset({"required", "not_required", "unknown"})
3333

3434

35+
def project_goal_write_scopes(
36+
project_root: str,
37+
allowed_scopes: list[str],
38+
) -> dict[str, Any]:
39+
"""Adapt observed local-root facts to the existing typed work owner."""
40+
result = _runtime_result(
41+
"goal_write_scopes",
42+
project_root=project_root,
43+
allowed_scopes=allowed_scopes,
44+
).get("write_scope_projection")
45+
if (
46+
not isinstance(result, Mapping)
47+
or not isinstance(result.get("allowed_write_scopes"), list)
48+
or any(not isinstance(scope, str) for scope in result["allowed_write_scopes"])
49+
):
50+
raise RuntimeError("TypeScript Goal write scope projection shape mismatch")
51+
return dict(result)
52+
53+
3554
def _runtime_result(operation: str, **params: Any) -> Mapping[str, Any]:
3655
try:
3756
result = effect_runtime_result(

‎loopx/control_plane/quota/settlement_workspace_causality.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../coordination/coordination_state_contract.generated.ts";
1010
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
1111
import { requireJsonObject as requiredObject } from "../runtime_decode.ts";
12+
import { projectGoalWriteScopes } from "../todos/work_requirements.ts";
1213

1314
export const DELIVERY_WORKSPACE_CAUSALITY_SCHEMA_VERSION =
1415
DELIVERY_WORKSPACE_CAUSALITY_SCHEMA;
@@ -38,6 +39,7 @@ export interface DeliveryWorkspaceCausality extends JsonObject {
3839
}
3940

4041
type DeliveryWorkspaceCausalityOperation =
42+
| "goal_write_scopes"
4143
| "classify"
4244
| "normalize"
4345
| "event_fields"
@@ -140,7 +142,7 @@ function operation(value: unknown): DeliveryWorkspaceCausalityOperation {
140142
value === "classify" || value === "normalize" ||
141143
value === "event_fields" || value === "missing_workspace" ||
142144
value === "settlement_requirement" ||
143-
value === "from_event"
145+
value === "from_event" || value === "goal_write_scopes"
144146
) return value;
145147
throw new EffectRuntimeRequestError("delivery workspace causality operation is unsupported");
146148
}
@@ -349,6 +351,9 @@ export function evaluateDeliveryWorkspaceCausality(value: unknown): JsonObject {
349351
request.expected_todo_id,
350352
"expected_todo_id",
351353
);
354+
if (selectedOperation === "goal_write_scopes") {
355+
return result({ write_scope_projection: projectGoalWriteScopes(request) });
356+
}
352357
if (selectedOperation === "classify") {
353358
return result({
354359
causality: classifyDeliveryWorkspaceCausality(

‎loopx/control_plane/quota/should_run.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
from ..agents.identity import (
1515
build_quota_agent_identity,
1616
)
17-
from ..agents.workspace_guard import build_agent_workspace_guard
17+
from ..agents.workspace_guard import (
18+
build_agent_workspace_guard,
19+
observe_goal_local_workspace,
20+
)
1821
from ..quota.decision_summary import (
1922
quota_plan_items as _quota_plan_items,
2023
)
@@ -114,16 +117,23 @@ def _apply_selected_todo_guards(
114117
)
115118
route = _resolve_quota_should_run_route(prepared)
116119
workspace_guard = None
120+
local_workspace = observe_goal_local_workspace(
121+
prepared.item, selected_todo, prepared.goal_boundary.get("write_scope", [])
122+
)
117123
if not prepared.inbox_priority_due:
118124
workspace_guard = build_agent_workspace_guard(
119125
prepared.item,
120126
prepared.agent_identity,
121127
agent_todo_summary=prepared.agent_todo_summary,
122128
selected_todo=selected_todo,
123129
current_path=workspace_path,
130+
local_workspace=local_workspace,
124131
)
125132
boundary_projection_repair = build_boundary_projection_repair_hint(
126-
prepared.goal_boundary,
133+
{**prepared.goal_boundary, "write_scope": [
134+
*prepared.goal_boundary.get("write_scope", []),
135+
*local_workspace.get("allowed_write_scopes", []),
136+
]},
127137
prepared.agent_todo_summary,
128138
candidate_should_run=bool(route.should_run),
129139
capability_gate=prepared.capability_gate,

‎loopx/control_plane/todos/work_requirements.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
55
import { optionalNonEmptyString, requireStringArray } from "../runtime_decode.ts";
66
import { compactPythonWhitespace, stripPythonWhitespace } from "../coordination/todo_agents.ts";
77
import { normalizeWriteScopes } from "../work_items/task_lease_acquire.ts";
8+
import { isAbsolute, resolve, sep } from "node:path";
9+
10+
/** Goal scope projection preserves existing relative scopes. Absolute grants
11+
* are projected only against the observed local Goal root, never by suffix or
12+
* against a different repository. This is a read projection, not a grant. */
13+
export function projectGoalWriteScopes(value: JsonObject): JsonObject {
14+
const root = optionalNonEmptyString(value.project_root, "project_root");
15+
if (!root || !isAbsolute(root)) return { allowed_write_scopes: [] };
16+
const prefix = resolve(root).replaceAll(sep, "/").replace(/\/+$/, "") + "/";
17+
const allowed: string[] = [];
18+
for (const raw of requireStringArray(value.allowed_scopes ?? [], "allowed_scopes")) {
19+
const scope = raw.replaceAll(sep, "/");
20+
// Only strip the registered root. Preserve the existing relative/glob
21+
// semantics; the boundary guard, not this projection, checks coverage.
22+
const relative = isAbsolute(scope)
23+
? (scope.startsWith(prefix) ? scope.slice(prefix.length) : "")
24+
: scope;
25+
if (normalizeWriteScopes([relative]).length && !allowed.includes(relative)) allowed.push(relative);
26+
}
27+
return { allowed_write_scopes: allowed };
28+
}
829

930
function optionalText(value: unknown, label: string): string | null {
1031
const raw = optionalNonEmptyString(value, label);

‎loopx/control_plane/work_items/interaction_contract.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ def _build_interaction_cli_channel(
14931493
"refresh": (
14941494
"delivery_workspace; otherwise --delivery-workspace-path"
14951495
if selected_todo.get("task_repository")
1496-
else "registered local Goal workspace; follow the current workspace guard and repository rules"
1496+
else "registered local Goal target; when caller cwd differs, refresh with --delivery-workspace-path for the actual target; spend uses the recorded delivery workspace"
14971497
),
14981498
"spend": "recorded_delivery_workspace",
14991499
"mismatch": "fail_closed",

0 commit comments

Comments
 (0)