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,45 @@
1
+ """MCP resource definitions and registration for CortexShift."""
2
+
3
+ import json
4
+
5
+ from mcp.server.mcpserver import MCPServer
6
+ from mcp.server.mcpserver.exceptions import ResourceError
7
+
8
+ from cortexshift.domain.errors import CortexShiftError
9
+ from cortexshift.mcp.facade import McpApplicationFacade
10
+
11
+
12
+ def register_resources(server: MCPServer, facade: McpApplicationFacade) -> None:
13
+ """Register read-only MCP resource views backed by McpApplicationFacade."""
14
+
15
+ @server.resource("cortexshift://project", mime_type="application/json")
16
+ def get_project_resource() -> str:
17
+ """Read-only view of project and bounded task/session summary."""
18
+ try:
19
+ return facade.get_project_context().model_dump_json(indent=2)
20
+ except (CortexShiftError, ValueError) as err:
21
+ raise ResourceError(f"Failed to read project resource: {err}") from err
22
+
23
+ @server.resource("cortexshift://task", mime_type="application/json")
24
+ def get_task_resource() -> str:
25
+ """Read-only view of the canonical bound task."""
26
+ try:
27
+ return json.dumps(facade.get_current_task(), indent=2)
28
+ except (CortexShiftError, ValueError) as err:
29
+ raise ResourceError(f"Failed to read task resource: {err}") from err
30
+
31
+ @server.resource("cortexshift://checkpoint/latest", mime_type="application/json")
32
+ def get_latest_checkpoint_resource() -> str:
33
+ """Read-only view of the latest checkpoint for the bound task."""
34
+ try:
35
+ return facade.get_latest_checkpoint().model_dump_json(indent=2)
36
+ except (CortexShiftError, ValueError) as err:
37
+ raise ResourceError(f"Failed to read checkpoint resource: {err}") from err
38
+
39
+ @server.resource("cortexshift://repository", mime_type="application/json")
40
+ def get_repository_resource() -> str:
41
+ """Read-only view of live Git repository status."""
42
+ try:
43
+ return json.dumps(facade.get_repository_status(), indent=2)
44
+ except (CortexShiftError, ValueError) as err:
45
+ raise ResourceError(f"Failed to read repository resource: {err}") from err
@@ -0,0 +1,52 @@
1
+ """MCPServer initialization and stdio execution for CortexShift."""
2
+
3
+ from mcp.server.mcpserver import MCPServer
4
+
5
+ from cortexshift.adapters.sqlite.store import SQLiteStateStore
6
+ from cortexshift.mcp.context import McpExecutionContext
7
+ from cortexshift.mcp.facade import McpApplicationFacade
8
+ from cortexshift.mcp.resources import register_resources
9
+ from cortexshift.mcp.tools import register_tools
10
+
11
+ SERVER_NAME = "cortexshift"
12
+ SERVER_TITLE = "CortexShift"
13
+
14
+ SERVER_INSTRUCTIONS = """\
15
+ CortexShift exposes structured project and task state.
16
+ Use repository files and live Git as the highest authority.
17
+ Use write tools after meaningful milestones to keep canonical Task state current.
18
+ Do not record trivial edits as milestones.
19
+ Checkpoint meaningful engineering decisions and reported test status.
20
+ """
21
+
22
+
23
+ def create_mcp_server(
24
+ context: McpExecutionContext,
25
+ store: SQLiteStateStore,
26
+ ) -> MCPServer:
27
+ """Create and configure a CortexShift MCPServer instance for the given context."""
28
+ server = MCPServer(
29
+ name=SERVER_NAME,
30
+ title=SERVER_TITLE,
31
+ instructions=SERVER_INSTRUCTIONS,
32
+ )
33
+
34
+ facade = McpApplicationFacade(context=context, store=store)
35
+ register_tools(server=server, facade=facade, context=context)
36
+ register_resources(server=server, facade=facade)
37
+
38
+ return server
39
+
40
+
41
+ def run_mcp_server(
42
+ context: McpExecutionContext,
43
+ store: SQLiteStateStore,
44
+ ) -> None:
45
+ """Run the CortexShift MCP server over stdio until EOF or process termination.
46
+
47
+ Invariants:
48
+ - Standard output is reserved strictly for the MCP wire protocol.
49
+ - Diagnostics and logs route exclusively to standard error.
50
+ """
51
+ server = create_mcp_server(context=context, store=store)
52
+ server.run(transport="stdio")
@@ -0,0 +1,176 @@
1
+ """MCP tool definitions and registration for CortexShift."""
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.mcpserver import MCPServer
6
+ from mcp.server.mcpserver.exceptions import ToolError
7
+
8
+ from cortexshift.domain.errors import CortexShiftError
9
+ from cortexshift.mcp.context import McpExecutionContext
10
+ from cortexshift.mcp.facade import McpApplicationFacade
11
+ from cortexshift.mcp.models import (
12
+ CheckpointResult,
13
+ CreateCheckpointResult,
14
+ DecisionResult,
15
+ ProjectContextResult,
16
+ TaskMutationResult,
17
+ )
18
+
19
+
20
+ def register_tools(
21
+ server: MCPServer,
22
+ facade: McpApplicationFacade,
23
+ context: McpExecutionContext,
24
+ ) -> None:
25
+ """Register read and write MCP tools based on execution context permissions."""
26
+
27
+ # -------------------------------------------------------------------------
28
+ # Read Tools (Always available in both managed and unmanaged contexts)
29
+ # -------------------------------------------------------------------------
30
+
31
+ @server.tool(
32
+ name="get_project_context",
33
+ description=(
34
+ "Get a structured, bounded overview of the current CortexShift project, "
35
+ "bound task, active session, latest checkpoint, and live repository status."
36
+ ),
37
+ )
38
+ def get_project_context() -> ProjectContextResult:
39
+ try:
40
+ return facade.get_project_context()
41
+ except (CortexShiftError, ValueError) as err:
42
+ raise ToolError(f"Failed to get project context: {err}") from err
43
+
44
+ @server.tool(
45
+ name="get_current_task",
46
+ description=(
47
+ "Get the complete canonical task record currently bound to this session, "
48
+ "including objective, requirements, completed work, remaining work, and known issues."
49
+ ),
50
+ )
51
+ def get_current_task() -> dict[str, Any]:
52
+ try:
53
+ return facade.get_current_task()
54
+ except (CortexShiftError, ValueError) as err:
55
+ raise ToolError(f"Failed to get current task: {err}") from err
56
+
57
+ @server.tool(
58
+ name="get_latest_checkpoint",
59
+ description=(
60
+ "Get the latest milestone, session-end, or recovery checkpoint for the bound task. "
61
+ "Returns null if no checkpoint exists."
62
+ ),
63
+ )
64
+ def get_latest_checkpoint() -> CheckpointResult:
65
+ try:
66
+ return facade.get_latest_checkpoint()
67
+ except (CortexShiftError, ValueError) as err:
68
+ raise ToolError(f"Failed to get latest checkpoint: {err}") from err
69
+
70
+ @server.tool(
71
+ name="get_repository_status",
72
+ description=(
73
+ "Inspect live Git repository status (branch, HEAD commit, dirty state, "
74
+ "staged/unstaged/untracked files, diff shortstat) without modifying state."
75
+ ),
76
+ )
77
+ def get_repository_status() -> dict[str, Any]:
78
+ try:
79
+ return facade.get_repository_status()
80
+ except (CortexShiftError, ValueError) as err:
81
+ raise ToolError(f"Failed to get repository status: {err}") from err
82
+
83
+ # -------------------------------------------------------------------------
84
+ # Write Tools (Registered only for managed, non-read-only sessions)
85
+ # -------------------------------------------------------------------------
86
+
87
+ if not context.can_mutate():
88
+ return
89
+
90
+ @server.tool(
91
+ name="set_current_work",
92
+ description=(
93
+ "Update the description of what the agent is currently working on. "
94
+ "Pass null or an empty string to clear."
95
+ ),
96
+ )
97
+ def set_current_work(current_work: str | None = None) -> TaskMutationResult:
98
+ try:
99
+ return facade.set_current_work(current_work)
100
+ except (CortexShiftError, ValueError) as err:
101
+ raise ToolError(f"Failed to set current work: {err}") from err
102
+
103
+ @server.tool(
104
+ name="mark_completed",
105
+ description=(
106
+ "Record completed work items on the canonical task. "
107
+ "Deduplicates items, appends to completed, and removes exact matches "
108
+ "from remaining. Does NOT mark the entire task completed."
109
+ ),
110
+ )
111
+ def mark_completed(items: list[str]) -> TaskMutationResult:
112
+ try:
113
+ return facade.mark_completed(items)
114
+ except (CortexShiftError, ValueError) as err:
115
+ raise ToolError(f"Failed to mark completed: {err}") from err
116
+
117
+ @server.tool(
118
+ name="add_remaining",
119
+ description=(
120
+ "Add new remaining work items to the canonical task. "
121
+ "Deduplicates items and preserves ordering."
122
+ ),
123
+ )
124
+ def add_remaining(items: list[str]) -> TaskMutationResult:
125
+ try:
126
+ return facade.add_remaining(items)
127
+ except (CortexShiftError, ValueError) as err:
128
+ raise ToolError(f"Failed to add remaining items: {err}") from err
129
+
130
+ @server.tool(
131
+ name="record_issue",
132
+ description=(
133
+ "Record a known technical issue, blocker, or edge case on the canonical task."
134
+ ),
135
+ )
136
+ def record_issue(items: list[str]) -> TaskMutationResult:
137
+ try:
138
+ return facade.record_issue(items)
139
+ except (CortexShiftError, ValueError) as err:
140
+ raise ToolError(f"Failed to record issue: {err}") from err
141
+
142
+ @server.tool(
143
+ name="record_decision",
144
+ description=(
145
+ "Record an important architectural or technical decision. "
146
+ "Captures an immutable milestone checkpoint containing the decision "
147
+ "and a live Git working tree snapshot."
148
+ ),
149
+ )
150
+ def record_decision(decision: str) -> DecisionResult:
151
+ try:
152
+ return facade.record_decision(decision)
153
+ except (CortexShiftError, ValueError) as err:
154
+ raise ToolError(f"Failed to record decision: {err}") from err
155
+
156
+ @server.tool(
157
+ name="create_checkpoint",
158
+ description=(
159
+ "Create a manual milestone checkpoint for the bound session. "
160
+ "Captures canonical task state, live Git snapshot, decisions, and "
161
+ "reported (unverified) test execution status."
162
+ ),
163
+ )
164
+ def create_checkpoint(
165
+ decisions: list[str] | None = None,
166
+ test_summary: str | None = None,
167
+ note: str | None = None,
168
+ ) -> CreateCheckpointResult:
169
+ try:
170
+ return facade.create_checkpoint(
171
+ decisions=decisions,
172
+ test_summary=test_summary,
173
+ note=note,
174
+ )
175
+ except (CortexShiftError, ValueError) as err:
176
+ raise ToolError(f"Failed to create checkpoint: {err}") from err
@@ -0,0 +1,39 @@
1
+ from cortexshift.ports.checkpoint_store import CheckpointStore
2
+ from cortexshift.ports.command_runner import CommandResult, CommandRunner
3
+ from cortexshift.ports.discovery import ProviderDiscoveryPort, ProviderProbe
4
+ from cortexshift.ports.handoff_delivery import (
5
+ HandoffDeliveryPreparation,
6
+ HandoffDeliveryStrategy,
7
+ ProviderHandoffAdapter,
8
+ )
9
+ from cortexshift.ports.handoff_store import HandoffStore
10
+ from cortexshift.ports.headless_runner import HeadlessProviderRunner, HeadlessResult
11
+ from cortexshift.ports.process_runner import InteractiveProcessRunner
12
+ from cortexshift.ports.provider import ProviderAdapter, ProviderRuntimeAdapter
13
+ from cortexshift.ports.repository import RepositoryInspector, RepositorySnapshotStore
14
+ from cortexshift.ports.session_store import SessionStore
15
+ from cortexshift.ports.state_store import StateStore
16
+ from cortexshift.ports.workspace_lease import WorkspaceLease, WorkspaceLeaseManager
17
+
18
+ __all__ = [
19
+ "CheckpointStore",
20
+ "CommandResult",
21
+ "CommandRunner",
22
+ "HandoffDeliveryPreparation",
23
+ "HandoffDeliveryStrategy",
24
+ "HandoffStore",
25
+ "HeadlessProviderRunner",
26
+ "HeadlessResult",
27
+ "InteractiveProcessRunner",
28
+ "ProviderAdapter",
29
+ "ProviderDiscoveryPort",
30
+ "ProviderHandoffAdapter",
31
+ "ProviderProbe",
32
+ "ProviderRuntimeAdapter",
33
+ "RepositoryInspector",
34
+ "RepositorySnapshotStore",
35
+ "SessionStore",
36
+ "StateStore",
37
+ "WorkspaceLease",
38
+ "WorkspaceLeaseManager",
39
+ ]
@@ -0,0 +1,45 @@
1
+ """Port defining persistence boundaries for canonical CortexShift checkpoints."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from cortexshift.domain.checkpoint import CheckpointRecord
6
+
7
+
8
+ @runtime_checkable
9
+ class CheckpointStore(Protocol):
10
+ """Abstract port for persisting and querying canonical checkpoint records.
11
+
12
+ Kept deliberately narrow and cohesive so the Phase 2 ``StateStore`` interface is not
13
+ re-expanded. A single concrete adapter may implement several persistence ports.
14
+ """
15
+
16
+ def save_checkpoint(self, checkpoint: CheckpointRecord) -> None:
17
+ """Persist a canonical CheckpointRecord including its payload."""
18
+ ...
19
+
20
+ def get_checkpoint(self, checkpoint_id: str) -> CheckpointRecord | None:
21
+ """Retrieve a CheckpointRecord by its stable identifier."""
22
+ ...
23
+
24
+ def list_checkpoints(
25
+ self,
26
+ project_id: str | None = None,
27
+ task_id: str | None = None,
28
+ limit: int | None = 20,
29
+ ) -> list[CheckpointRecord]:
30
+ """List checkpoint records, ordered newest first."""
31
+ ...
32
+
33
+ def get_latest_checkpoint(
34
+ self,
35
+ task_id: str,
36
+ ) -> CheckpointRecord | None:
37
+ """Retrieve the newest checkpoint record for a given task."""
38
+ ...
39
+
40
+ def list_task_checkpoint_history(
41
+ self,
42
+ task_id: str,
43
+ ) -> list[CheckpointRecord]:
44
+ """List every checkpoint for one task, oldest first, with deterministic ordering."""
45
+ ...
@@ -0,0 +1,56 @@
1
+ """Port defining the interface for safe external command execution."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+
9
+ class CommandResult(BaseModel):
10
+ """Result of an executed external command."""
11
+
12
+ model_config = ConfigDict(frozen=True)
13
+
14
+ command: list[str]
15
+ exit_code: int
16
+ stdout: str
17
+ stderr: str
18
+ timed_out: bool = False
19
+ not_found: bool = False
20
+ error_message: str | None = None
21
+
22
+ @property
23
+ def success(self) -> bool:
24
+ """Whether the command exited with returncode 0."""
25
+ return self.exit_code == 0 and not self.timed_out and not self.not_found
26
+
27
+
28
+ @runtime_checkable
29
+ class CommandRunner(Protocol):
30
+ """Abstract port for executing native CLI commands safely.
31
+
32
+ Implementations must avoid shell=True, execute commands as argument lists,
33
+ enforce finite timeouts, and handle process failures gracefully.
34
+ """
35
+
36
+ def run(
37
+ self,
38
+ command: list[str],
39
+ timeout: float = 5.0,
40
+ env: dict[str, str] | None = None,
41
+ cwd: Path | str | None = None,
42
+ sanitize: bool = True,
43
+ ) -> CommandResult:
44
+ """Execute a command as an argument list with a finite timeout.
45
+
46
+ Args:
47
+ command: Command and arguments as a list of strings.
48
+ timeout: Maximum execution duration in seconds.
49
+ env: Optional environment dictionary override.
50
+ cwd: Optional working directory for command execution.
51
+ sanitize: Whether to sanitize, strip ANSI escapes, and bound stdout/stderr.
52
+
53
+ Returns:
54
+ CommandResult containing exit code, stdout, stderr, and failure flags.
55
+ """
56
+ ...
@@ -0,0 +1,41 @@
1
+ """Port defining the interface for discovering and probing AI coding agent providers."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from cortexshift.domain.doctor import ProviderDiagnostic
6
+ from cortexshift.domain.provider import ProviderId
7
+
8
+
9
+ @runtime_checkable
10
+ class ProviderProbe(Protocol):
11
+ """Abstract port for probing a single provider CLI."""
12
+
13
+ @property
14
+ def provider_id(self) -> ProviderId:
15
+ """Canonical provider identifier."""
16
+ ...
17
+
18
+ def probe(self) -> ProviderDiagnostic:
19
+ """Probe the provider CLI and return its diagnostic status."""
20
+ ...
21
+
22
+
23
+ @runtime_checkable
24
+ class ProviderDiscoveryPort(Protocol):
25
+ """Abstract port for discovering and probing native provider CLIs."""
26
+
27
+ def discover_all(self) -> list[ProviderDiagnostic]:
28
+ """Discover and probe all registered providers."""
29
+ ...
30
+
31
+ def discover_provider(self, provider_id: ProviderId) -> ProviderDiagnostic:
32
+ """Discover and probe a specific provider by its ID.
33
+
34
+ Raises:
35
+ KeyError: If the provider is not supported/registered.
36
+ """
37
+ ...
38
+
39
+ def get_supported_provider_ids(self) -> list[ProviderId]:
40
+ """Return the list of all supported canonical provider IDs."""
41
+ ...
@@ -0,0 +1,91 @@
1
+ """Port defining provider-specific delivery of a canonical handoff context package.
2
+
3
+ The application core knows only the target provider, the rendered canonical context, and
4
+ the project root. How that context physically reaches a given native CLI (a positional
5
+ initial prompt, or a read-only headless bootstrap followed by a conversation resume) is
6
+ encapsulated entirely behind this port, so orchestration services never branch on
7
+ provider identity.
8
+ """
9
+
10
+ from enum import StrEnum
11
+ from pathlib import Path
12
+ from typing import Protocol, runtime_checkable
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+
16
+ from cortexshift.domain.launch import LaunchSpecification
17
+ from cortexshift.domain.provider import ProviderId
18
+
19
+
20
+ class HandoffDeliveryStrategy(StrEnum):
21
+ """Documented native transport used to deliver a handoff to a provider.
22
+
23
+ - DIRECT_INITIAL_PROMPT: the canonical context is passed to the provider's native
24
+ interactive CLI as a single positional initial-prompt argument.
25
+ - PLAN_BOOTSTRAP_THEN_RESUME: the canonical context is ingested by one read-only
26
+ headless planning turn, and the resulting native conversation is then resumed
27
+ interactively.
28
+ """
29
+
30
+ DIRECT_INITIAL_PROMPT = "direct_initial_prompt"
31
+ READ_ONLY_BOOTSTRAP_THEN_RESUME = "read_only_bootstrap_then_resume"
32
+ PLAN_BOOTSTRAP_THEN_RESUME = "plan_bootstrap_then_resume"
33
+
34
+
35
+ class HandoffDeliveryPreparation(BaseModel):
36
+ """Outcome of preparing handoff delivery for a target provider."""
37
+
38
+ model_config = ConfigDict(frozen=True)
39
+
40
+ launch_spec: LaunchSpecification
41
+ native_session_id: str | None = None
42
+ bootstrap_performed: bool = False
43
+
44
+
45
+ @runtime_checkable
46
+ class ProviderHandoffAdapter(Protocol):
47
+ """Abstract port for delivering canonical handoff context through a native CLI."""
48
+
49
+ @property
50
+ def provider_id(self) -> ProviderId:
51
+ """Canonical provider identifier."""
52
+ ...
53
+
54
+ @property
55
+ def display_name(self) -> str:
56
+ """Human-readable provider name."""
57
+ ...
58
+
59
+ @property
60
+ def executable(self) -> str:
61
+ """Base name of the provider CLI executable."""
62
+ ...
63
+
64
+ @property
65
+ def delivery_strategy(self) -> HandoffDeliveryStrategy:
66
+ """Documented native transport used by this provider."""
67
+ ...
68
+
69
+ @property
70
+ def bootstrap_model_turn_required(self) -> bool:
71
+ """Whether delivery consumes one provider model turn before interactive launch."""
72
+ ...
73
+
74
+ def prepare_delivery(
75
+ self,
76
+ executable_path: str,
77
+ project_root: Path,
78
+ rendered_context: str,
79
+ native_session_id: str | None = None,
80
+ ) -> HandoffDeliveryPreparation:
81
+ """Deliver the canonical context and return the interactive launch specification.
82
+
83
+ Implementations performing a headless bootstrap must parse only the minimum
84
+ required machine fields and discard the provider response without persisting
85
+ or logging it.
86
+
87
+ Raises:
88
+ HandoffDeliveryError: If delivery could not be completed. The error carries a
89
+ safe machine classification and never embeds raw provider output.
90
+ """
91
+ ...
@@ -0,0 +1,43 @@
1
+ """Port defining persistence boundaries for canonical CortexShift handoffs."""
2
+
3
+ from datetime import datetime
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from cortexshift.domain.handoff import HandoffFailureCode, HandoffRecord, HandoffStatus
7
+
8
+
9
+ @runtime_checkable
10
+ class HandoffStore(Protocol):
11
+ """Abstract port for persisting and querying canonical handoff records.
12
+
13
+ Kept deliberately narrow and cohesive so the Phase 2 ``StateStore`` interface is not
14
+ re-expanded. A single concrete adapter may implement several persistence ports.
15
+ """
16
+
17
+ def save_handoff(self, handoff: HandoffRecord) -> None:
18
+ """Persist or update a canonical HandoffRecord including its payload."""
19
+ ...
20
+
21
+ def update_handoff_delivery(
22
+ self,
23
+ handoff_id: str,
24
+ status: HandoffStatus,
25
+ target_session_id: str | None = None,
26
+ delivered_at: datetime | None = None,
27
+ failure_code: HandoffFailureCode | None = None,
28
+ ) -> None:
29
+ """Update the delivery metadata of an existing handoff record."""
30
+ ...
31
+
32
+ def get_handoff(self, handoff_id: str) -> HandoffRecord | None:
33
+ """Retrieve a HandoffRecord by its stable identifier."""
34
+ ...
35
+
36
+ def list_handoffs(
37
+ self,
38
+ project_id: str | None = None,
39
+ task_id: str | None = None,
40
+ limit: int = 20,
41
+ ) -> list[HandoffRecord]:
42
+ """List handoff records, ordered newest first."""
43
+ ...
@@ -0,0 +1,58 @@
1
+ """Port defining bounded headless execution of a single provider model turn.
2
+
3
+ Distinct from the short-timeout diagnostic ``CommandRunner`` used by provider discovery:
4
+ a headless provider bootstrap is one full model turn and legitimately needs minutes,
5
+ while still being strictly non-interactive, shell-free, and silent by default.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Protocol, runtime_checkable
10
+
11
+ from pydantic import BaseModel, ConfigDict
12
+
13
+ # A single provider planning turn may legitimately take minutes. The bound stays finite
14
+ # so a hung provider can never wedge a CortexShift switch indefinitely.
15
+ DEFAULT_HEADLESS_TIMEOUT_SECONDS = 300.0
16
+
17
+
18
+ class HeadlessResult(BaseModel):
19
+ """Result of a bounded headless provider invocation.
20
+
21
+ Captured output is returned to the caller for minimal machine-field parsing only.
22
+ Adapters must discard it after extracting required metadata and must never persist
23
+ or log it.
24
+ """
25
+
26
+ model_config = ConfigDict(frozen=True)
27
+
28
+ exit_code: int
29
+ stdout: str
30
+ stderr: str
31
+ timed_out: bool = False
32
+ not_found: bool = False
33
+
34
+
35
+ @runtime_checkable
36
+ class HeadlessProviderRunner(Protocol):
37
+ """Abstract port for running one non-interactive provider turn without a TTY."""
38
+
39
+ def run_headless(
40
+ self,
41
+ argv: list[str],
42
+ cwd: Path | str,
43
+ timeout: float = DEFAULT_HEADLESS_TIMEOUT_SECONDS,
44
+ env: dict[str, str] | None = None,
45
+ ) -> HeadlessResult:
46
+ """Execute a provider process headlessly, capturing stdout and stderr.
47
+
48
+ Args:
49
+ argv: Argument vector executed directly, never through a shell.
50
+ cwd: Canonical working directory (the project root).
51
+ timeout: Finite upper bound appropriate for a single model turn.
52
+ env: Optional environment overlay.
53
+
54
+ Returns:
55
+ A HeadlessResult describing the outcome. Implementations must not raise
56
+ on process failure, timeout, or a missing executable.
57
+ """
58
+ ...
@@ -0,0 +1,20 @@
1
+ """Optional exact-resume boundary implemented only by capable runtime adapters."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from cortexshift.domain.launch import LaunchSpecification
7
+ from cortexshift.domain.native_session import NativeSessionCapabilities
8
+
9
+
10
+ @runtime_checkable
11
+ class ProviderNativeSessionAdapter(Protocol):
12
+ def get_native_capabilities(self) -> NativeSessionCapabilities:
13
+ """Describe reliable identity and resume mechanisms."""
14
+ ...
15
+
16
+ def build_exact_resume(
17
+ self, project_root: Path, executable_path: str, native_session_id: str
18
+ ) -> LaunchSpecification:
19
+ """Build an exact native resume, without a model turn or process launch."""
20
+ ...