cortexshift 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cortexshift/__init__.py +10 -0
- cortexshift/__main__.py +6 -0
- cortexshift/adapters/__init__.py +22 -0
- cortexshift/adapters/command_runner.py +116 -0
- cortexshift/adapters/discovery.py +55 -0
- cortexshift/adapters/git/__init__.py +10 -0
- cortexshift/adapters/git/inspector.py +321 -0
- cortexshift/adapters/git/parser.py +140 -0
- cortexshift/adapters/headless_runner.py +92 -0
- cortexshift/adapters/process_runner.py +56 -0
- cortexshift/adapters/providers/__init__.py +4 -0
- cortexshift/adapters/providers/antigravity.py +530 -0
- cortexshift/adapters/providers/claude.py +375 -0
- cortexshift/adapters/providers/codex.py +434 -0
- cortexshift/adapters/sqlite/__init__.py +10 -0
- cortexshift/adapters/sqlite/migrations.py +268 -0
- cortexshift/adapters/sqlite/store.py +914 -0
- cortexshift/adapters/workspace_lease.py +123 -0
- cortexshift/application/__init__.py +42 -0
- cortexshift/application/checkpoint_builder.py +218 -0
- cortexshift/application/checkpoint_service.py +273 -0
- cortexshift/application/doctor.py +80 -0
- cortexshift/application/handoff_builder.py +281 -0
- cortexshift/application/handoff_renderer.py +430 -0
- cortexshift/application/handoff_service.py +66 -0
- cortexshift/application/init_service.py +86 -0
- cortexshift/application/locator.py +48 -0
- cortexshift/application/native_session.py +65 -0
- cortexshift/application/recovery_service.py +235 -0
- cortexshift/application/repository_service.py +146 -0
- cortexshift/application/resume_service.py +124 -0
- cortexshift/application/run_service.py +270 -0
- cortexshift/application/session_launcher.py +183 -0
- cortexshift/application/session_service.py +63 -0
- cortexshift/application/source_session.py +62 -0
- cortexshift/application/status_service.py +73 -0
- cortexshift/application/switch_service.py +671 -0
- cortexshift/application/task_service.py +201 -0
- cortexshift/application/task_workspace.py +152 -0
- cortexshift/cli/__init__.py +5 -0
- cortexshift/cli/app.py +2477 -0
- cortexshift/domain/__init__.py +153 -0
- cortexshift/domain/checkpoint.py +174 -0
- cortexshift/domain/doctor.py +68 -0
- cortexshift/domain/errors.py +277 -0
- cortexshift/domain/git.py +102 -0
- cortexshift/domain/handoff.py +241 -0
- cortexshift/domain/identifiers.py +27 -0
- cortexshift/domain/launch.py +58 -0
- cortexshift/domain/mcp_binding.py +81 -0
- cortexshift/domain/native_session.py +19 -0
- cortexshift/domain/project.py +37 -0
- cortexshift/domain/provider.py +67 -0
- cortexshift/domain/session.py +92 -0
- cortexshift/domain/status.py +40 -0
- cortexshift/domain/task.py +191 -0
- cortexshift/mcp/__init__.py +38 -0
- cortexshift/mcp/context.py +165 -0
- cortexshift/mcp/facade.py +513 -0
- cortexshift/mcp/models.py +178 -0
- cortexshift/mcp/resources.py +45 -0
- cortexshift/mcp/server.py +52 -0
- cortexshift/mcp/tools.py +176 -0
- cortexshift/ports/__init__.py +39 -0
- cortexshift/ports/checkpoint_store.py +45 -0
- cortexshift/ports/command_runner.py +56 -0
- cortexshift/ports/discovery.py +41 -0
- cortexshift/ports/handoff_delivery.py +91 -0
- cortexshift/ports/handoff_store.py +43 -0
- cortexshift/ports/headless_runner.py +58 -0
- cortexshift/ports/native_session.py +20 -0
- cortexshift/ports/process_runner.py +31 -0
- cortexshift/ports/provider.py +152 -0
- cortexshift/ports/repository.py +44 -0
- cortexshift/ports/session_store.py +27 -0
- cortexshift/ports/state_store.py +55 -0
- cortexshift/ports/workspace_lease.py +39 -0
- cortexshift/tui/__init__.py +24 -0
- cortexshift/tui/actions.py +58 -0
- cortexshift/tui/app.py +1051 -0
- cortexshift/tui/coordinator.py +173 -0
- cortexshift/tui/cortexshift.tcss +258 -0
- cortexshift/tui/facade.py +614 -0
- cortexshift/tui/modals.py +594 -0
- cortexshift/tui/models.py +503 -0
- cortexshift/tui/screens/__init__.py +81 -0
- cortexshift/tui/screens/checkpoints.py +188 -0
- cortexshift/tui/screens/handoffs.py +180 -0
- cortexshift/tui/screens/help.py +117 -0
- cortexshift/tui/screens/overview.py +200 -0
- cortexshift/tui/screens/providers.py +169 -0
- cortexshift/tui/screens/repository.py +143 -0
- cortexshift/tui/screens/sessions.py +146 -0
- cortexshift/tui/screens/task.py +174 -0
- cortexshift/tui/widgets.py +209 -0
- cortexshift-0.1.0.dist-info/METADATA +202 -0
- cortexshift-0.1.0.dist-info/RECORD +100 -0
- cortexshift-0.1.0.dist-info/WHEEL +4 -0
- cortexshift-0.1.0.dist-info/entry_points.txt +2 -0
- cortexshift-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Application service managing Task lifecycle and active task pointer."""
|
|
2
|
+
|
|
3
|
+
from cortexshift.domain.errors import (
|
|
4
|
+
NoActiveTaskError,
|
|
5
|
+
TaskAlreadyCompletedError,
|
|
6
|
+
TaskNotActivatableError,
|
|
7
|
+
TaskNotFoundError,
|
|
8
|
+
)
|
|
9
|
+
from cortexshift.domain.task import Task, TaskStatus
|
|
10
|
+
from cortexshift.ports.state_store import StateStore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskService:
|
|
14
|
+
"""Orchestrates persistent Task creation, updates, and activation transitions."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, store: StateStore) -> None:
|
|
17
|
+
self.store = store
|
|
18
|
+
|
|
19
|
+
def start_task(
|
|
20
|
+
self,
|
|
21
|
+
project_id: str,
|
|
22
|
+
title: str,
|
|
23
|
+
objective: str,
|
|
24
|
+
requirements: list[str] | None = None,
|
|
25
|
+
constraints: list[str] | None = None,
|
|
26
|
+
set_active: bool = True,
|
|
27
|
+
) -> Task:
|
|
28
|
+
"""Create a new task, persist it, and optionally set it as the active task.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project_id: Canonical ID of the owning project.
|
|
32
|
+
title: Short descriptive task title.
|
|
33
|
+
objective: High-level goal and scope of the task.
|
|
34
|
+
requirements: Optional initial list of requirement strings.
|
|
35
|
+
constraints: Optional initial list of constraint strings.
|
|
36
|
+
set_active: Whether to set this task as active immediately (defaults to True).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The created Task entity.
|
|
40
|
+
"""
|
|
41
|
+
initial_status = TaskStatus.IN_PROGRESS if set_active else TaskStatus.PENDING
|
|
42
|
+
task = Task(
|
|
43
|
+
project_id=project_id,
|
|
44
|
+
title=title,
|
|
45
|
+
objective=objective,
|
|
46
|
+
requirements=requirements or [],
|
|
47
|
+
constraints=constraints or [],
|
|
48
|
+
status=initial_status,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.store.save_task(task)
|
|
52
|
+
if set_active:
|
|
53
|
+
self.store.set_active_task_id(project_id, task.id)
|
|
54
|
+
return task
|
|
55
|
+
|
|
56
|
+
def get_task(self, task_id: str) -> Task:
|
|
57
|
+
"""Retrieve a task by identifier.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
TaskNotFoundError: If the task does not exist.
|
|
61
|
+
"""
|
|
62
|
+
task = self.store.get_task(task_id)
|
|
63
|
+
if task is None:
|
|
64
|
+
raise TaskNotFoundError(task_id)
|
|
65
|
+
return task
|
|
66
|
+
|
|
67
|
+
def get_active_task(self, project_id: str) -> Task | None:
|
|
68
|
+
"""Retrieve the currently active task for a project, or None if no task is active."""
|
|
69
|
+
active_id = self.store.get_active_task_id(project_id)
|
|
70
|
+
if active_id is None:
|
|
71
|
+
return None
|
|
72
|
+
return self.store.get_task(active_id)
|
|
73
|
+
|
|
74
|
+
def list_tasks(self, project_id: str) -> list[Task]:
|
|
75
|
+
"""List all tasks associated with a project."""
|
|
76
|
+
return self.store.list_tasks(project_id)
|
|
77
|
+
|
|
78
|
+
def activate_task(self, project_id: str, task_id: str) -> Task:
|
|
79
|
+
"""Make an existing task the active task for the project.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
project_id: Canonical ID of the owning project.
|
|
83
|
+
task_id: Identifier of the task to activate.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The newly activated Task entity.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
TaskNotFoundError: If the task does not exist or does not belong to the project.
|
|
90
|
+
TaskNotActivatableError: If the task is in a terminal status (completed/cancelled).
|
|
91
|
+
"""
|
|
92
|
+
task = self.get_task(task_id)
|
|
93
|
+
if task.project_id != project_id:
|
|
94
|
+
raise TaskNotFoundError(task_id)
|
|
95
|
+
|
|
96
|
+
if task.is_terminal:
|
|
97
|
+
msg = (
|
|
98
|
+
f"Cannot activate task '{task_id}' because it is in terminal status "
|
|
99
|
+
f"'{task.status.value}'."
|
|
100
|
+
)
|
|
101
|
+
raise TaskNotActivatableError(msg)
|
|
102
|
+
|
|
103
|
+
self.store.set_active_task_id(project_id, task.id)
|
|
104
|
+
return task
|
|
105
|
+
|
|
106
|
+
def complete_task(self, project_id: str, task_id: str | None = None) -> Task:
|
|
107
|
+
"""Mark a task as completed.
|
|
108
|
+
|
|
109
|
+
If task_id is None, the currently active task is completed.
|
|
110
|
+
If the completed task is currently active, the active task pointer is cleared.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
project_id: Canonical ID of the owning project.
|
|
114
|
+
task_id: Optional identifier of the task to complete.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The updated Task entity.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
NoActiveTaskError: If task_id is None and no task is currently active.
|
|
121
|
+
TaskNotFoundError: If the specified task does not exist.
|
|
122
|
+
TaskAlreadyCompletedError: If the task is already completed.
|
|
123
|
+
"""
|
|
124
|
+
if task_id is None:
|
|
125
|
+
active_id = self.store.get_active_task_id(project_id)
|
|
126
|
+
if active_id is None:
|
|
127
|
+
raise NoActiveTaskError("No active task to complete.")
|
|
128
|
+
target_task = self.get_task(active_id)
|
|
129
|
+
else:
|
|
130
|
+
target_task = self.get_task(task_id)
|
|
131
|
+
if target_task.project_id != project_id:
|
|
132
|
+
raise TaskNotFoundError(task_id)
|
|
133
|
+
|
|
134
|
+
if target_task.status == TaskStatus.COMPLETED:
|
|
135
|
+
raise TaskAlreadyCompletedError(target_task.id)
|
|
136
|
+
|
|
137
|
+
completed_task = target_task.mark_completed()
|
|
138
|
+
self.store.save_task(completed_task)
|
|
139
|
+
|
|
140
|
+
# Clear active task pointer if this task was active
|
|
141
|
+
active_id = self.store.get_active_task_id(project_id)
|
|
142
|
+
if active_id == completed_task.id:
|
|
143
|
+
self.store.set_active_task_id(project_id, None)
|
|
144
|
+
|
|
145
|
+
return completed_task
|
|
146
|
+
|
|
147
|
+
def update_task(
|
|
148
|
+
self,
|
|
149
|
+
project_id: str,
|
|
150
|
+
task_id: str | None = None,
|
|
151
|
+
current_work: str | None = None,
|
|
152
|
+
clear_current_work: bool = False,
|
|
153
|
+
add_completed: list[str] | None = None,
|
|
154
|
+
add_remaining: list[str] | None = None,
|
|
155
|
+
add_issues: list[str] | None = None,
|
|
156
|
+
) -> Task:
|
|
157
|
+
"""Update fields and progression lists on a task.
|
|
158
|
+
|
|
159
|
+
Defaults to modifying the active task if task_id is None.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
project_id: Canonical ID of the owning project.
|
|
163
|
+
task_id: Optional specific task identifier.
|
|
164
|
+
current_work: Optional new text describing in-flight work.
|
|
165
|
+
clear_current_work: If True, clears current_work to None.
|
|
166
|
+
add_completed: Optional list of completed item strings to append.
|
|
167
|
+
add_remaining: Optional list of remaining item strings to append.
|
|
168
|
+
add_issues: Optional list of known issue strings to append.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The updated Task entity.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
NoActiveTaskError: If task_id is None and no task is active.
|
|
175
|
+
TaskNotFoundError: If the specified task does not exist.
|
|
176
|
+
"""
|
|
177
|
+
if task_id is None:
|
|
178
|
+
active_id = self.store.get_active_task_id(project_id)
|
|
179
|
+
if active_id is None:
|
|
180
|
+
raise NoActiveTaskError("No active task to update.")
|
|
181
|
+
target_task = self.get_task(active_id)
|
|
182
|
+
else:
|
|
183
|
+
target_task = self.get_task(task_id)
|
|
184
|
+
if target_task.project_id != project_id:
|
|
185
|
+
raise TaskNotFoundError(task_id)
|
|
186
|
+
|
|
187
|
+
updated = target_task
|
|
188
|
+
if clear_current_work:
|
|
189
|
+
updated = updated.with_current_work(None)
|
|
190
|
+
elif current_work is not None:
|
|
191
|
+
updated = updated.with_current_work(current_work)
|
|
192
|
+
|
|
193
|
+
if add_completed:
|
|
194
|
+
updated = updated.add_completed_items(add_completed)
|
|
195
|
+
if add_remaining:
|
|
196
|
+
updated = updated.add_remaining_items(add_remaining)
|
|
197
|
+
if add_issues:
|
|
198
|
+
updated = updated.add_known_issues(add_issues)
|
|
199
|
+
|
|
200
|
+
self.store.save_task(updated)
|
|
201
|
+
return updated
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Project-root scoped Task operations with managed persistence lifecycle.
|
|
2
|
+
|
|
3
|
+
`TaskService` operates on an already-open `StateStore`. Outer adapters that address a
|
|
4
|
+
project by path — the TUI control center in particular — must not open SQLite themselves,
|
|
5
|
+
so this service resolves the initialized project root, manages store lifecycle, and
|
|
6
|
+
delegates every rule to `TaskService` and the canonical `Task` domain model.
|
|
7
|
+
|
|
8
|
+
It contains no task business rules of its own.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from cortexshift.adapters.sqlite.store import SQLiteStateStore
|
|
17
|
+
from cortexshift.application.locator import ProjectLocator
|
|
18
|
+
from cortexshift.application.task_service import TaskService
|
|
19
|
+
from cortexshift.domain.errors import NoActiveTaskError, ProjectNotInitializedError
|
|
20
|
+
from cortexshift.domain.project import Project
|
|
21
|
+
from cortexshift.domain.task import Task
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ResolvedTaskWorkspace:
|
|
26
|
+
"""An opened project workspace bound to a single canonical Project."""
|
|
27
|
+
|
|
28
|
+
project_root: Path
|
|
29
|
+
project: Project
|
|
30
|
+
tasks: TaskService
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TaskWorkspaceService:
|
|
34
|
+
"""Resolves a project by path and applies canonical Task operations to it."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, project_locator: type[ProjectLocator] = ProjectLocator) -> None:
|
|
37
|
+
self._locator = project_locator
|
|
38
|
+
|
|
39
|
+
@contextmanager
|
|
40
|
+
def open(self, start_dir: Path | str | None = None) -> Iterator[ResolvedTaskWorkspace]:
|
|
41
|
+
"""Open the nearest initialized project and yield its Task service.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ProjectNotInitializedError: If no initialized project root is found.
|
|
45
|
+
"""
|
|
46
|
+
start_path = Path(start_dir) if start_dir is not None else None
|
|
47
|
+
project_root = self._locator.find_project_root(start_path)
|
|
48
|
+
if project_root is None:
|
|
49
|
+
raise ProjectNotInitializedError()
|
|
50
|
+
|
|
51
|
+
db_path = self._locator.get_database_path(project_root)
|
|
52
|
+
store = SQLiteStateStore(db_path, auto_migrate=False)
|
|
53
|
+
try:
|
|
54
|
+
project = store.get_default_project()
|
|
55
|
+
if project is None:
|
|
56
|
+
raise ProjectNotInitializedError()
|
|
57
|
+
yield ResolvedTaskWorkspace(
|
|
58
|
+
project_root=project_root,
|
|
59
|
+
project=project,
|
|
60
|
+
tasks=TaskService(store),
|
|
61
|
+
)
|
|
62
|
+
finally:
|
|
63
|
+
store.close()
|
|
64
|
+
|
|
65
|
+
def list_tasks(self, start_dir: Path | str | None = None) -> list[Task]:
|
|
66
|
+
"""List every Task belonging to the resolved project."""
|
|
67
|
+
with self.open(start_dir) as workspace:
|
|
68
|
+
return workspace.tasks.list_tasks(workspace.project.id)
|
|
69
|
+
|
|
70
|
+
def get_active_task(self, start_dir: Path | str | None = None) -> Task | None:
|
|
71
|
+
"""Return the active Task for the resolved project, or None."""
|
|
72
|
+
with self.open(start_dir) as workspace:
|
|
73
|
+
return workspace.tasks.get_active_task(workspace.project.id)
|
|
74
|
+
|
|
75
|
+
def start_task(
|
|
76
|
+
self,
|
|
77
|
+
title: str,
|
|
78
|
+
objective: str,
|
|
79
|
+
requirements: list[str] | None = None,
|
|
80
|
+
constraints: list[str] | None = None,
|
|
81
|
+
set_active: bool = True,
|
|
82
|
+
start_dir: Path | str | None = None,
|
|
83
|
+
) -> Task:
|
|
84
|
+
"""Create a new Task using the canonical creation rules."""
|
|
85
|
+
with self.open(start_dir) as workspace:
|
|
86
|
+
return workspace.tasks.start_task(
|
|
87
|
+
project_id=workspace.project.id,
|
|
88
|
+
title=title,
|
|
89
|
+
objective=objective,
|
|
90
|
+
requirements=requirements,
|
|
91
|
+
constraints=constraints,
|
|
92
|
+
set_active=set_active,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def activate_task(self, task_id: str, start_dir: Path | str | None = None) -> Task:
|
|
96
|
+
"""Activate an existing Task using canonical activation rules.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
TaskNotFoundError: If the Task does not belong to the resolved project.
|
|
100
|
+
TaskNotActivatableError: If the Task is in a terminal status.
|
|
101
|
+
"""
|
|
102
|
+
with self.open(start_dir) as workspace:
|
|
103
|
+
return workspace.tasks.activate_task(workspace.project.id, task_id)
|
|
104
|
+
|
|
105
|
+
def set_current_work(
|
|
106
|
+
self,
|
|
107
|
+
current_work: str | None,
|
|
108
|
+
start_dir: Path | str | None = None,
|
|
109
|
+
) -> Task:
|
|
110
|
+
"""Replace the active Task's in-flight work description."""
|
|
111
|
+
with self.open(start_dir) as workspace:
|
|
112
|
+
return workspace.tasks.update_task(
|
|
113
|
+
project_id=workspace.project.id,
|
|
114
|
+
current_work=current_work,
|
|
115
|
+
clear_current_work=current_work is None,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def mark_completed(self, items: list[str], start_dir: Path | str | None = None) -> Task:
|
|
119
|
+
"""Mark items completed on the active Task and drop them from remaining.
|
|
120
|
+
|
|
121
|
+
Applies the canonical `Task.complete_items` rule — the same rule MCP agents use —
|
|
122
|
+
so the Task itself is never completed as a side effect.
|
|
123
|
+
"""
|
|
124
|
+
with self.open(start_dir) as workspace:
|
|
125
|
+
task = self._require_active(workspace)
|
|
126
|
+
updated = task.complete_items(items)
|
|
127
|
+
workspace.tasks.store.save_task(updated)
|
|
128
|
+
return updated
|
|
129
|
+
|
|
130
|
+
def add_remaining(self, items: list[str], start_dir: Path | str | None = None) -> Task:
|
|
131
|
+
"""Append newly discovered remaining items to the active Task."""
|
|
132
|
+
with self.open(start_dir) as workspace:
|
|
133
|
+
return workspace.tasks.update_task(
|
|
134
|
+
project_id=workspace.project.id,
|
|
135
|
+
add_remaining=items,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def record_issues(self, items: list[str], start_dir: Path | str | None = None) -> Task:
|
|
139
|
+
"""Record known issues or blockers on the active Task."""
|
|
140
|
+
with self.open(start_dir) as workspace:
|
|
141
|
+
return workspace.tasks.update_task(
|
|
142
|
+
project_id=workspace.project.id,
|
|
143
|
+
add_issues=items,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _require_active(workspace: ResolvedTaskWorkspace) -> Task:
|
|
148
|
+
"""Return the active Task or fail with the canonical error."""
|
|
149
|
+
task = workspace.tasks.get_active_task(workspace.project.id)
|
|
150
|
+
if task is None:
|
|
151
|
+
raise NoActiveTaskError("No active task to update.")
|
|
152
|
+
return task
|