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.
Files changed (100) hide show
  1. cortexshift/__init__.py +10 -0
  2. cortexshift/__main__.py +6 -0
  3. cortexshift/adapters/__init__.py +22 -0
  4. cortexshift/adapters/command_runner.py +116 -0
  5. cortexshift/adapters/discovery.py +55 -0
  6. cortexshift/adapters/git/__init__.py +10 -0
  7. cortexshift/adapters/git/inspector.py +321 -0
  8. cortexshift/adapters/git/parser.py +140 -0
  9. cortexshift/adapters/headless_runner.py +92 -0
  10. cortexshift/adapters/process_runner.py +56 -0
  11. cortexshift/adapters/providers/__init__.py +4 -0
  12. cortexshift/adapters/providers/antigravity.py +530 -0
  13. cortexshift/adapters/providers/claude.py +375 -0
  14. cortexshift/adapters/providers/codex.py +434 -0
  15. cortexshift/adapters/sqlite/__init__.py +10 -0
  16. cortexshift/adapters/sqlite/migrations.py +268 -0
  17. cortexshift/adapters/sqlite/store.py +914 -0
  18. cortexshift/adapters/workspace_lease.py +123 -0
  19. cortexshift/application/__init__.py +42 -0
  20. cortexshift/application/checkpoint_builder.py +218 -0
  21. cortexshift/application/checkpoint_service.py +273 -0
  22. cortexshift/application/doctor.py +80 -0
  23. cortexshift/application/handoff_builder.py +281 -0
  24. cortexshift/application/handoff_renderer.py +430 -0
  25. cortexshift/application/handoff_service.py +66 -0
  26. cortexshift/application/init_service.py +86 -0
  27. cortexshift/application/locator.py +48 -0
  28. cortexshift/application/native_session.py +65 -0
  29. cortexshift/application/recovery_service.py +235 -0
  30. cortexshift/application/repository_service.py +146 -0
  31. cortexshift/application/resume_service.py +124 -0
  32. cortexshift/application/run_service.py +270 -0
  33. cortexshift/application/session_launcher.py +183 -0
  34. cortexshift/application/session_service.py +63 -0
  35. cortexshift/application/source_session.py +62 -0
  36. cortexshift/application/status_service.py +73 -0
  37. cortexshift/application/switch_service.py +671 -0
  38. cortexshift/application/task_service.py +201 -0
  39. cortexshift/application/task_workspace.py +152 -0
  40. cortexshift/cli/__init__.py +5 -0
  41. cortexshift/cli/app.py +2477 -0
  42. cortexshift/domain/__init__.py +153 -0
  43. cortexshift/domain/checkpoint.py +174 -0
  44. cortexshift/domain/doctor.py +68 -0
  45. cortexshift/domain/errors.py +277 -0
  46. cortexshift/domain/git.py +102 -0
  47. cortexshift/domain/handoff.py +241 -0
  48. cortexshift/domain/identifiers.py +27 -0
  49. cortexshift/domain/launch.py +58 -0
  50. cortexshift/domain/mcp_binding.py +81 -0
  51. cortexshift/domain/native_session.py +19 -0
  52. cortexshift/domain/project.py +37 -0
  53. cortexshift/domain/provider.py +67 -0
  54. cortexshift/domain/session.py +92 -0
  55. cortexshift/domain/status.py +40 -0
  56. cortexshift/domain/task.py +191 -0
  57. cortexshift/mcp/__init__.py +38 -0
  58. cortexshift/mcp/context.py +165 -0
  59. cortexshift/mcp/facade.py +513 -0
  60. cortexshift/mcp/models.py +178 -0
  61. cortexshift/mcp/resources.py +45 -0
  62. cortexshift/mcp/server.py +52 -0
  63. cortexshift/mcp/tools.py +176 -0
  64. cortexshift/ports/__init__.py +39 -0
  65. cortexshift/ports/checkpoint_store.py +45 -0
  66. cortexshift/ports/command_runner.py +56 -0
  67. cortexshift/ports/discovery.py +41 -0
  68. cortexshift/ports/handoff_delivery.py +91 -0
  69. cortexshift/ports/handoff_store.py +43 -0
  70. cortexshift/ports/headless_runner.py +58 -0
  71. cortexshift/ports/native_session.py +20 -0
  72. cortexshift/ports/process_runner.py +31 -0
  73. cortexshift/ports/provider.py +152 -0
  74. cortexshift/ports/repository.py +44 -0
  75. cortexshift/ports/session_store.py +27 -0
  76. cortexshift/ports/state_store.py +55 -0
  77. cortexshift/ports/workspace_lease.py +39 -0
  78. cortexshift/tui/__init__.py +24 -0
  79. cortexshift/tui/actions.py +58 -0
  80. cortexshift/tui/app.py +1051 -0
  81. cortexshift/tui/coordinator.py +173 -0
  82. cortexshift/tui/cortexshift.tcss +258 -0
  83. cortexshift/tui/facade.py +614 -0
  84. cortexshift/tui/modals.py +594 -0
  85. cortexshift/tui/models.py +503 -0
  86. cortexshift/tui/screens/__init__.py +81 -0
  87. cortexshift/tui/screens/checkpoints.py +188 -0
  88. cortexshift/tui/screens/handoffs.py +180 -0
  89. cortexshift/tui/screens/help.py +117 -0
  90. cortexshift/tui/screens/overview.py +200 -0
  91. cortexshift/tui/screens/providers.py +169 -0
  92. cortexshift/tui/screens/repository.py +143 -0
  93. cortexshift/tui/screens/sessions.py +146 -0
  94. cortexshift/tui/screens/task.py +174 -0
  95. cortexshift/tui/widgets.py +209 -0
  96. cortexshift-0.1.0.dist-info/METADATA +202 -0
  97. cortexshift-0.1.0.dist-info/RECORD +100 -0
  98. cortexshift-0.1.0.dist-info/WHEEL +4 -0
  99. cortexshift-0.1.0.dist-info/entry_points.txt +2 -0
  100. 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
@@ -0,0 +1,5 @@
1
+ """CLI package for CortexShift."""
2
+
3
+ from cortexshift.cli.app import app
4
+
5
+ __all__ = ["app"]