Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions project/ticket-119/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Ticket 119: Separate Goal workspace repository selection

- **ID**: ticket-119
- **Owner**: unresolved:human
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-09-08

SESSION_EXECUTION_AUTHORIZATION: User requested continued refactoring, GitHub publication, deployment and testing.

AC-01: Extract bounded repository enumeration and dirty-repository selection
from Goal workspace resolution while preserving fail-closed ambiguity and
unreadable-status errors.

## Acceptance criteria

- [ ] AC-01: Goal target resolution remains behaviorally identical.

Validation: 10 focused tests, Koru-driven regressions, Ruff, managed
governance, Docker Compose and compileall passed.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
29 changes: 29 additions & 0 deletions project/ticket-119/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-119",
"summary": "Separate Goal workspace repository selection",
"workstream": "application",
"classification": {
"kind": "SERVICE",
"priority": "P2",
"origin": "requested"
},
"allowedPaths": ["src/koru/goal_workspace.py", "tests/test_goal_workspace.py", "project/ticket-119/**"],
"forbiddenPaths": ["project/ticket-*/user-*.md"],
"stacks": ["python", "docker"],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "9c609d3fb5702ff0e41c848f6d384180cbe8bc9f",
"targetBranch": "main",
"outcome": "Publish a behavior-preserving Goal workspace selection extraction.",
"nonGoals": ["No changes to target authority, fail-closed rules, dependencies or public interfaces."],
"complexity": "S",
"estimatedMinutes": 30,
"budgets": {"maxImplementationFiles": 2, "maxAffectedComponents": 1, "maxPublicInterfaceChanges": 0, "maxRuntimeDependencies": 0},
"architecture": {"status": "accepted", "decision": "Move child repository discovery and dirty selection into a focused helper while retaining resolution errors and explicit repo handling.", "components": [{"name": "goal-workspace", "paths": ["src/koru/goal_workspace.py", "tests/test_goal_workspace.py"]}], "responsibilityChanges": false, "interfaceChanges": [], "dataChanges": [], "ui": {"impact": "none", "states": [], "evidence": []}, "rollback": "Revert protected merge."},
"runtimeDependencies": [],
"validation": [{"criterion": "AC-01", "commands": ["python -m pytest -q tests/test_goal_workspace.py", "./project/governance-check.sh", "docker compose config --quiet"], "evidence": "Focused regression, Koru run and independent CI."}]
}
}
69 changes: 36 additions & 33 deletions src/koru/goal_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ def _explicit_project(
return candidate


def _select_workspace_repository(
workspace: Path, repositories: tuple[Path, ...], *,
runner: Callable[..., subprocess.CompletedProcess[str]],
) -> Path:
"""Select the sole dirty child or fail closed on ambiguity."""
dirty: list[Path] = []
unreadable: list[str] = []
for candidate in repositories:
state = _is_dirty(candidate, runner=runner)
if state is None:
unreadable.append(candidate.name)
elif state:
dirty.append(candidate)
if unreadable:
raise GoalProjectResolutionError(
"Git status failed for workspace repositories; select only after inspection",
workspace, tuple(unreadable),
)
if len(dirty) == 1:
return dirty[0]
if not repositories:
raise GoalProjectResolutionError(
"--project is neither a Git repository nor an umbrella with Git children", workspace,
)
if not dirty:
raise GoalProjectResolutionError(
"umbrella workspace has no dirty repository; use --repo to select one",
workspace, tuple(candidate.name for candidate in repositories),
)
raise GoalProjectResolutionError(
"umbrella workspace has multiple dirty repositories; use --repo to select one",
workspace, tuple(candidate.name for candidate in dirty),
)


def resolve_goal_project(
project: Path,
repo: str | None = None,
Expand Down Expand Up @@ -152,36 +187,4 @@ def resolve_goal_project(
and not child.is_symlink()
and _is_git_root(child, runner=runner)
)
dirty: list[Path] = []
unreadable: list[str] = []
for candidate in repositories:
state = _is_dirty(candidate, runner=runner)
if state is None:
unreadable.append(candidate.name)
elif state:
dirty.append(candidate)

if unreadable:
raise GoalProjectResolutionError(
"Git status failed for workspace repositories; select only after inspection",
workspace,
tuple(unreadable),
)
if len(dirty) == 1:
return dirty[0]
if not repositories:
raise GoalProjectResolutionError(
"--project is neither a Git repository nor an umbrella with Git children",
workspace,
)
if not dirty:
raise GoalProjectResolutionError(
"umbrella workspace has no dirty repository; use --repo to select one",
workspace,
tuple(candidate.name for candidate in repositories),
)
raise GoalProjectResolutionError(
"umbrella workspace has multiple dirty repositories; use --repo to select one",
workspace,
tuple(candidate.name for candidate in dirty),
)
return _select_workspace_repository(workspace, repositories, runner=runner)
Loading