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,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")
|
cortexshift/mcp/tools.py
ADDED
|
@@ -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
|
+
...
|