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,80 @@
1
+ """Application service for running environment and provider diagnostics."""
2
+
3
+ import platform
4
+ from datetime import UTC, datetime
5
+
6
+ from cortexshift import __version__
7
+ from cortexshift.adapters.discovery import BuiltinProviderDiscovery
8
+ from cortexshift.domain.doctor import DoctorReport, PlatformInfo, ProviderDiagnostic
9
+ from cortexshift.domain.provider import ProviderId
10
+ from cortexshift.ports.discovery import ProviderDiscoveryPort
11
+
12
+
13
+ class UnknownProviderError(ValueError):
14
+ """Raised when an unrecognized provider ID is requested for diagnostics."""
15
+
16
+ def __init__(self, provider_id: str, supported: list[str]) -> None:
17
+ self.provider_id = provider_id
18
+ self.supported = supported
19
+ super().__init__(
20
+ f"Unknown provider '{provider_id}'. Supported providers: {', '.join(supported)}"
21
+ )
22
+
23
+
24
+ class DoctorService:
25
+ """Orchestrates environment discovery and provider diagnostics.
26
+
27
+ Collects platform metadata and delegates provider probing to a
28
+ ProviderDiscoveryPort implementation. Does not interact directly with CLI
29
+ frameworks, Rich rendering, or raw subprocess calls.
30
+ """
31
+
32
+ def __init__(self, discovery: ProviderDiscoveryPort | None = None) -> None:
33
+ self._discovery = discovery or BuiltinProviderDiscovery()
34
+
35
+ def run_diagnostics(
36
+ self,
37
+ provider_ids: list[ProviderId] | None = None,
38
+ ) -> DoctorReport:
39
+ """Run diagnostics for all or selected providers.
40
+
41
+ Args:
42
+ provider_ids: Optional list of provider IDs to filter diagnostics by.
43
+ If None or empty, all supported providers are diagnosed.
44
+
45
+ Returns:
46
+ DoctorReport containing platform metadata and provider diagnostics.
47
+
48
+ Raises:
49
+ UnknownProviderError: If any specified provider ID is not supported.
50
+ """
51
+ supported_ids = self._discovery.get_supported_provider_ids()
52
+ supported_str_list = [str(pid) for pid in supported_ids]
53
+
54
+ provider_diagnostics: list[ProviderDiagnostic] = []
55
+
56
+ if provider_ids:
57
+ # Validate all requested providers first
58
+ for pid in provider_ids:
59
+ if pid not in supported_ids:
60
+ raise UnknownProviderError(str(pid), supported_str_list)
61
+
62
+ for pid in provider_ids:
63
+ provider_diagnostics.append(self._discovery.discover_provider(pid))
64
+ else:
65
+ provider_diagnostics = self._discovery.discover_all()
66
+
67
+ platform_info = PlatformInfo(
68
+ system=platform.system(),
69
+ release=platform.release(),
70
+ machine=platform.machine(),
71
+ python_version=platform.python_version(),
72
+ )
73
+
74
+ return DoctorReport(
75
+ cortexshift_version=__version__,
76
+ python_version=platform.python_version(),
77
+ platform=platform_info,
78
+ timestamp=datetime.now(UTC),
79
+ providers=provider_diagnostics,
80
+ )
@@ -0,0 +1,281 @@
1
+ """Deterministic construction of canonical handoff payloads from durable local state.
2
+
3
+ The builder is pure: it performs no I/O, invokes no provider, and consumes no model
4
+ quota. Its only inputs are the canonical Project, the canonical Task, previous
5
+ CortexShift Session metadata, a live repository inspection, and the identifier of the
6
+ Git snapshot persisted at handoff time.
7
+
8
+ This is the mechanism that makes CortexShift work after the outgoing agent is already
9
+ gone: nothing here requires the outgoing provider to be installed, running, or able to
10
+ answer a question.
11
+ """
12
+
13
+ from collections.abc import Iterable
14
+
15
+ from cortexshift.domain.checkpoint import CheckpointRecord
16
+ from cortexshift.domain.git import RepositoryInspection, RepositoryInspectionStatus
17
+ from cortexshift.domain.handoff import (
18
+ HANDOFF_PROTOCOL_VERSION,
19
+ MAX_OPERATOR_NOTE_CHARS,
20
+ HandoffGitState,
21
+ HandoffPayload,
22
+ HandoffSourceSession,
23
+ HandoffTestStatus,
24
+ )
25
+ from cortexshift.domain.identifiers import utc_now
26
+ from cortexshift.domain.project import Project
27
+ from cortexshift.domain.provider import ProviderId
28
+ from cortexshift.domain.session import Session
29
+ from cortexshift.domain.task import Task
30
+
31
+ # Emitted only when the task's entire checkpoint history holds no structured decision.
32
+ # CortexShift encodes a genuine absence honestly rather than inferring facts it cannot
33
+ # support — and, equally, never claims absence over decisions it actually holds.
34
+ UNKNOWN_DECISIONS_STATEMENT = "No structured decisions are recorded in CortexShift state."
35
+ UNKNOWN_TEST_STATUS_STATEMENT = (
36
+ "No verified test result is recorded in CortexShift state. "
37
+ "The receiving agent must run relevant tests before relying on previous claims."
38
+ )
39
+
40
+ _GIT_NOTE_READY = (
41
+ "Live Git inspection succeeded at handoff time. "
42
+ "This is a historical observation, not current truth; re-check with `git status`."
43
+ )
44
+ _GIT_NOTE_NOT_INSTALLED = (
45
+ "Git was not found on this machine at handoff time. No repository state could be observed."
46
+ )
47
+ _GIT_NOTE_NOT_REPOSITORY = (
48
+ "This CortexShift project is not inside a Git repository. "
49
+ "No repository state could be observed."
50
+ )
51
+ _GIT_NOTE_PROBE_ERROR = (
52
+ "Git repository inspection failed at handoff time. "
53
+ "Treat any repository claim below as unverified and inspect the workspace directly."
54
+ )
55
+
56
+ _GIT_NOTES: dict[RepositoryInspectionStatus, str] = {
57
+ RepositoryInspectionStatus.READY: _GIT_NOTE_READY,
58
+ RepositoryInspectionStatus.GIT_NOT_INSTALLED: _GIT_NOTE_NOT_INSTALLED,
59
+ RepositoryInspectionStatus.NOT_GIT_REPOSITORY: _GIT_NOTE_NOT_REPOSITORY,
60
+ RepositoryInspectionStatus.PROBE_ERROR: _GIT_NOTE_PROBE_ERROR,
61
+ }
62
+
63
+
64
+ def aggregate_task_decisions(
65
+ checkpoints: Iterable[CheckpointRecord],
66
+ task_id: str,
67
+ ) -> list[str]:
68
+ """Aggregate the structured decisions recorded across one task's checkpoint history.
69
+
70
+ A checkpoint is an immutable point-in-time observation and is never rewritten, so a
71
+ decision recorded by `record_decision` lives only in the checkpoint that minted it.
72
+ Task-level durability is therefore reconstructed here, at handoff time, by reading the
73
+ task's checkpoint history instead of only its newest checkpoint.
74
+
75
+ Semantics:
76
+ - **Chronological first-seen order.** Callers supply checkpoints oldest first; the
77
+ resulting order follows the order in which each decision first entered state.
78
+ - **Exact-equality de-duplication.** A decision string repeated across checkpoints is
79
+ emitted once, at its first occurrence. Text is never normalised, trimmed for
80
+ comparison, or fuzzy-matched — two decisions differing by a single character stay
81
+ distinct.
82
+ - **Task isolation.** Records whose `task_id` does not match are dropped, so a store
83
+ query that ever widened its scope still could not leak another task's decisions.
84
+
85
+ This function is pure: it performs no I/O and mutates nothing it is given.
86
+ """
87
+ aggregated: list[str] = []
88
+ seen: set[str] = set()
89
+ for record in checkpoints:
90
+ if record.task_id != task_id:
91
+ continue
92
+ for decision in record.payload.decisions:
93
+ if not decision.strip():
94
+ continue
95
+ if decision in seen:
96
+ continue
97
+ seen.add(decision)
98
+ aggregated.append(decision)
99
+ return aggregated
100
+
101
+
102
+ def derive_files_touched(inspection: RepositoryInspection) -> list[str]:
103
+ """Derive the FILES TOUCHED list from live Git inspection.
104
+
105
+ Deduplicates the union of staged, modified, untracked, and conflicted paths while
106
+ preserving a deterministic order. Source files are never opened to populate this
107
+ field, and full diffs are never read or stored.
108
+ """
109
+ snapshot = inspection.snapshot
110
+ if snapshot is None:
111
+ return []
112
+
113
+ ordered: list[str] = []
114
+ seen: set[str] = set()
115
+ for group in (
116
+ snapshot.staged_files,
117
+ snapshot.modified_files,
118
+ snapshot.untracked_files,
119
+ snapshot.conflicted_files,
120
+ ):
121
+ for path in group:
122
+ if path not in seen:
123
+ seen.add(path)
124
+ ordered.append(path)
125
+ return ordered
126
+
127
+
128
+ def build_git_state(
129
+ inspection: RepositoryInspection,
130
+ snapshot_id: str | None = None,
131
+ ) -> HandoffGitState:
132
+ """Build the canonical GIT STATE section, honestly encoding unavailable Git."""
133
+ note = _GIT_NOTES.get(inspection.status, _GIT_NOTE_PROBE_ERROR)
134
+ snapshot = inspection.snapshot
135
+
136
+ if inspection.status != RepositoryInspectionStatus.READY or snapshot is None:
137
+ return HandoffGitState(status=inspection.status, available=False, note=note)
138
+
139
+ return HandoffGitState(
140
+ status=inspection.status,
141
+ available=True,
142
+ note=note,
143
+ branch=snapshot.branch,
144
+ head_sha=snapshot.head_sha,
145
+ detached_head=snapshot.detached_head,
146
+ dirty=snapshot.dirty,
147
+ staged_count=len(snapshot.staged_files),
148
+ modified_count=len(snapshot.modified_files),
149
+ untracked_count=len(snapshot.untracked_files),
150
+ conflicted_count=len(snapshot.conflicted_files),
151
+ working_tree_diff_summary=snapshot.working_tree_diff_summary,
152
+ staged_diff_summary=snapshot.staged_diff_summary,
153
+ snapshot_id=snapshot_id,
154
+ )
155
+
156
+
157
+ def derive_recommended_next_action(task: Task) -> str:
158
+ """Derive the RECOMMENDED NEXT ACTION deterministically from canonical task state.
159
+
160
+ Priority: current work, then the first remaining item, then repository inspection.
161
+ No model call is ever used to produce this recommendation.
162
+ """
163
+ current = (task.current_work or "").strip()
164
+ if current:
165
+ return f"Continue the work already in progress: {current}"
166
+
167
+ for item in task.remaining_items:
168
+ candidate = item.strip()
169
+ if candidate:
170
+ return f"Start the next recorded remaining item: {candidate}"
171
+
172
+ return (
173
+ "Inspect the current repository state (git status, changed files, and the "
174
+ "project's relevant tests) and determine the next incomplete step toward the "
175
+ "original objective."
176
+ )
177
+
178
+
179
+ def bound_operator_note(note: str | None) -> str | None:
180
+ """Bound an optional operator note so it can never make the handoff unbounded."""
181
+ if note is None:
182
+ return None
183
+ cleaned = note.strip()
184
+ if not cleaned:
185
+ return None
186
+ if len(cleaned) > MAX_OPERATOR_NOTE_CHARS:
187
+ omitted = len(cleaned) - MAX_OPERATOR_NOTE_CHARS
188
+ return f"{cleaned[:MAX_OPERATOR_NOTE_CHARS]}… [{omitted} characters omitted]"
189
+ return cleaned
190
+
191
+
192
+ class HandoffBuilder:
193
+ """Builds canonical handoff payloads deterministically from durable state."""
194
+
195
+ def build(
196
+ self,
197
+ project: Project,
198
+ task: Task,
199
+ source_session: Session,
200
+ inspection: RepositoryInspection,
201
+ target_provider_id: ProviderId,
202
+ snapshot_id: str | None = None,
203
+ operator_note: str | None = None,
204
+ latest_checkpoint: CheckpointRecord | None = None,
205
+ task_decisions: list[str] | None = None,
206
+ ) -> HandoffPayload:
207
+ """Build the canonical handoff payload for a task moving to another provider.
208
+
209
+ `task_decisions` carries the decisions aggregated across the task's whole
210
+ checkpoint history (see `aggregate_task_decisions`) and is authoritative when
211
+ supplied — including when it is empty, which asserts that the task genuinely has
212
+ no recorded decision. `latest_checkpoint` remains the provenance reference for the
213
+ snapshot and reported test status; it is used as the decision source only when no
214
+ aggregate was supplied at all.
215
+ """
216
+ important_decisions: list[str] = []
217
+ decisions_known: bool = False
218
+ if task_decisions is not None:
219
+ important_decisions = list(task_decisions)
220
+ decisions_known = bool(important_decisions)
221
+ elif latest_checkpoint is not None and latest_checkpoint.payload.decisions:
222
+ important_decisions = list(latest_checkpoint.payload.decisions)
223
+ decisions_known = True
224
+
225
+ test_status: HandoffTestStatus
226
+ if latest_checkpoint is not None and latest_checkpoint.payload.test_status.known:
227
+ test_status = HandoffTestStatus(
228
+ known=True,
229
+ summary=(
230
+ f"Checkpoint-reported test status:\n"
231
+ f"{latest_checkpoint.payload.test_status.summary}\n\n"
232
+ f"This result was not independently verified by CortexShift. "
233
+ f"Re-run relevant tests before relying on it."
234
+ ),
235
+ )
236
+ else:
237
+ test_status = HandoffTestStatus(known=False, summary=UNKNOWN_TEST_STATUS_STATEMENT)
238
+
239
+ source_checkpoint_id = latest_checkpoint.id if latest_checkpoint else None
240
+ source_checkpoint_kind = latest_checkpoint.kind.value if latest_checkpoint else None
241
+ source_checkpoint_created_at = latest_checkpoint.created_at if latest_checkpoint else None
242
+
243
+ return HandoffPayload(
244
+ protocol_version=HANDOFF_PROTOCOL_VERSION,
245
+ generated_at=utc_now(),
246
+ project_name=project.name,
247
+ project_root=project.repo_path,
248
+ task_id=task.id,
249
+ task_title=task.title,
250
+ task_status=task.status.value,
251
+ original_objective=task.objective,
252
+ requirements=list(task.requirements),
253
+ constraints=list(task.constraints),
254
+ completed=list(task.completed_items),
255
+ current_work=task.current_work,
256
+ remaining=list(task.remaining_items),
257
+ important_decisions=important_decisions,
258
+ decisions_known=decisions_known,
259
+ files_touched=derive_files_touched(inspection),
260
+ test_status=test_status,
261
+ known_issues=list(task.known_issues),
262
+ git_state=build_git_state(inspection, snapshot_id=snapshot_id),
263
+ # Completed work is the only signal CortexShift durably holds about what
264
+ # should not be rebuilt. It remains advisory and must still be verified.
265
+ do_not_redo=list(task.completed_items),
266
+ recommended_next_action=derive_recommended_next_action(task),
267
+ source_session=HandoffSourceSession(
268
+ session_id=source_session.id,
269
+ provider_id=source_session.provider_id,
270
+ status=source_session.status,
271
+ started_at=source_session.started_at,
272
+ ended_at=source_session.ended_at,
273
+ exit_reason=source_session.exit_reason,
274
+ exit_code=source_session.exit_code,
275
+ ),
276
+ target_provider_id=target_provider_id,
277
+ operator_note=bound_operator_note(operator_note),
278
+ source_checkpoint_id=source_checkpoint_id,
279
+ source_checkpoint_kind=source_checkpoint_kind,
280
+ source_checkpoint_created_at=source_checkpoint_created_at,
281
+ )