scc-cli 1.5.3__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.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (153) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,309 @@
1
+ """
2
+ Launch render functions - pure output with no business logic.
3
+
4
+ This module contains display/rendering functions extracted from launch.py.
5
+ These are pure output functions that format and display information.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+
16
+ from ... import git
17
+ from ...cli_common import MAX_DISPLAY_PATH_LENGTH, PATH_TRUNCATE_LENGTH, console
18
+ from ...output_mode import print_human
19
+ from ...theme import Indicators
20
+
21
+ if TYPE_CHECKING:
22
+ from .workspace import LaunchContext
23
+
24
+
25
+ def warn_if_non_worktree(workspace_path: Path | None, *, json_mode: bool = False) -> None:
26
+ """Warn when running from a main repo without a worktree.
27
+
28
+ Args:
29
+ workspace_path: Path to the workspace directory, or None.
30
+ json_mode: If True, suppress the warning.
31
+ """
32
+ import sys
33
+
34
+ if json_mode or workspace_path is None:
35
+ return
36
+
37
+ if not git.is_git_repo(workspace_path):
38
+ return
39
+
40
+ if git.is_worktree(workspace_path):
41
+ return
42
+
43
+ print_human(
44
+ "[yellow]Tip:[/yellow] You're working in the main repo. "
45
+ "For isolation, try: scc worktree create . <feature> or "
46
+ "scc start --worktree <feature>",
47
+ file=sys.stderr,
48
+ highlight=False,
49
+ )
50
+
51
+
52
+ def build_dry_run_data(
53
+ workspace_path: Path,
54
+ team: str | None,
55
+ org_config: dict[str, Any] | None,
56
+ project_config: dict[str, Any] | None,
57
+ *,
58
+ entry_dir: Path | None = None,
59
+ mount_root: Path | None = None,
60
+ container_workdir: str | None = None,
61
+ resolution_reason: str | None = None,
62
+ ) -> dict[str, Any]:
63
+ """
64
+ Build dry run data showing resolved configuration.
65
+
66
+ This pure function assembles configuration information for preview
67
+ without performing any side effects like Docker launch.
68
+
69
+ Args:
70
+ workspace_path: Path to the workspace root (WR).
71
+ team: Selected team profile name (or None).
72
+ org_config: Organization configuration dict (or None).
73
+ project_config: Project-level .scc.yaml config (or None).
74
+ entry_dir: Entry directory (ED), defaults to workspace_path if not provided.
75
+ mount_root: Mount root (MR), defaults to workspace_path if not provided.
76
+ container_workdir: Container workdir (CW), defaults to entry_dir if not provided.
77
+ resolution_reason: Debug explanation for how workspace was resolved.
78
+
79
+ Returns:
80
+ Dictionary with resolved configuration data including path information.
81
+ """
82
+ plugins: list[dict[str, Any]] = []
83
+ blocked_items: list[str] = []
84
+
85
+ if org_config and team:
86
+ from ... import profiles
87
+
88
+ workspace_for_project = None if project_config is not None else workspace_path
89
+ effective = profiles.compute_effective_config(
90
+ org_config,
91
+ team,
92
+ project_config=project_config,
93
+ workspace_path=workspace_for_project,
94
+ )
95
+
96
+ for plugin in sorted(effective.plugins):
97
+ plugins.append({"name": plugin, "source": "resolved"})
98
+
99
+ for blocked in effective.blocked_items:
100
+ if blocked.blocked_by:
101
+ blocked_items.append(f"{blocked.item} (blocked by '{blocked.blocked_by}')")
102
+ else:
103
+ blocked_items.append(blocked.item)
104
+
105
+ # Compute defaults for optional path fields
106
+ effective_entry = entry_dir if entry_dir is not None else workspace_path
107
+ effective_mount = mount_root if mount_root is not None else workspace_path
108
+ effective_cw = container_workdir if container_workdir is not None else str(effective_entry)
109
+
110
+ return {
111
+ "workspace_root": str(workspace_path),
112
+ "entry_dir": str(effective_entry),
113
+ "mount_root": str(effective_mount),
114
+ "container_workdir": effective_cw,
115
+ "team": team,
116
+ "plugins": plugins,
117
+ "blocked_items": blocked_items,
118
+ "ready_to_start": len(blocked_items) == 0,
119
+ "resolution_reason": resolution_reason,
120
+ }
121
+
122
+
123
+ def show_launch_panel(
124
+ workspace: Path | None,
125
+ team: str | None,
126
+ session_name: str | None,
127
+ branch: str | None,
128
+ is_resume: bool,
129
+ ) -> None:
130
+ """Display launch info panel with session details.
131
+
132
+ Args:
133
+ workspace: Path to the workspace directory, or None.
134
+ team: Team profile name, or None for base profile.
135
+ session_name: Optional session name for identification.
136
+ branch: Current git branch, or None if not in a git repo.
137
+ is_resume: True if resuming an existing container.
138
+ """
139
+ grid = Table.grid(padding=(0, 2))
140
+ grid.add_column(style="dim", no_wrap=True)
141
+ grid.add_column(style="white")
142
+
143
+ if workspace:
144
+ # Shorten path for display
145
+ display_path = str(workspace)
146
+ if len(display_path) > MAX_DISPLAY_PATH_LENGTH:
147
+ display_path = "..." + display_path[-PATH_TRUNCATE_LENGTH:]
148
+ grid.add_row("Workspace:", display_path)
149
+
150
+ grid.add_row("Team:", team or "standalone")
151
+
152
+ if branch:
153
+ grid.add_row("Branch:", branch)
154
+
155
+ if session_name:
156
+ grid.add_row("Session:", session_name)
157
+
158
+ mode = "[green]Resume existing[/green]" if is_resume else "[cyan]New container[/cyan]"
159
+ grid.add_row("Mode:", mode)
160
+
161
+ panel = Panel(
162
+ grid,
163
+ title="[bold green]Launching Claude Code[/bold green]",
164
+ border_style="green",
165
+ padding=(0, 1),
166
+ )
167
+
168
+ console.print()
169
+ console.print(panel)
170
+ console.print()
171
+ console.print("[dim]Starting Docker sandbox...[/dim]")
172
+ console.print()
173
+
174
+
175
+ def show_dry_run_panel(data: dict[str, Any]) -> None:
176
+ """Display dry run configuration preview.
177
+
178
+ Args:
179
+ data: Dictionary containing workspace paths, team, plugins, and ready_to_start status.
180
+ """
181
+ grid = Table.grid(padding=(0, 2))
182
+ grid.add_column(style="dim", no_wrap=True)
183
+ grid.add_column(style="white")
184
+
185
+ # Workspace root (WR)
186
+ workspace_root = data.get("workspace_root", data.get("workspace", ""))
187
+ if len(workspace_root) > MAX_DISPLAY_PATH_LENGTH:
188
+ workspace_root = "..." + workspace_root[-PATH_TRUNCATE_LENGTH:]
189
+ grid.add_row("Workspace root:", workspace_root)
190
+
191
+ # Entry dir (ED) - only show if different from workspace_root
192
+ entry_dir = data.get("entry_dir", "")
193
+ if entry_dir and entry_dir != data.get("workspace_root"):
194
+ if len(entry_dir) > MAX_DISPLAY_PATH_LENGTH:
195
+ entry_dir = "..." + entry_dir[-PATH_TRUNCATE_LENGTH:]
196
+ grid.add_row("Entry dir:", entry_dir)
197
+
198
+ # Mount root (MR) - only show if different (worktree expansion)
199
+ mount_root = data.get("mount_root", "")
200
+ if mount_root and mount_root != data.get("workspace_root"):
201
+ if len(mount_root) > MAX_DISPLAY_PATH_LENGTH:
202
+ mount_root = "..." + mount_root[-PATH_TRUNCATE_LENGTH:]
203
+ grid.add_row("Mount root:", f"{mount_root} [dim](worktree)[/dim]")
204
+
205
+ # Container workdir (CW)
206
+ container_workdir = data.get("container_workdir", "")
207
+ if container_workdir:
208
+ if len(container_workdir) > MAX_DISPLAY_PATH_LENGTH:
209
+ container_workdir = "..." + container_workdir[-PATH_TRUNCATE_LENGTH:]
210
+ grid.add_row("Container cwd:", container_workdir)
211
+
212
+ # Team
213
+ grid.add_row("Team:", data.get("team") or "standalone")
214
+
215
+ # Plugins
216
+ plugins = data.get("plugins", [])
217
+ if plugins:
218
+ plugin_list = ", ".join(p.get("name", "unknown") for p in plugins)
219
+ grid.add_row("Plugins:", plugin_list)
220
+ else:
221
+ grid.add_row("Plugins:", "[dim]none[/dim]")
222
+
223
+ # Ready status
224
+ ready = data.get("ready_to_start", True)
225
+ status = (
226
+ f"[green]{Indicators.get('PASS')} Ready to start[/green]"
227
+ if ready
228
+ else f"[red]{Indicators.get('FAIL')} Blocked[/red]"
229
+ )
230
+ grid.add_row("Status:", status)
231
+
232
+ # Blocked items
233
+ blocked = data.get("blocked_items", [])
234
+ if blocked:
235
+ for item in blocked:
236
+ grid.add_row("[red]Blocked:[/red]", item)
237
+
238
+ panel = Panel(
239
+ grid,
240
+ title="[bold cyan]Dry Run Preview[/bold cyan]",
241
+ border_style="cyan",
242
+ padding=(0, 1),
243
+ )
244
+
245
+ console.print()
246
+ console.print(panel)
247
+ console.print()
248
+ if ready:
249
+ console.print("[dim]Remove --dry-run to launch[/dim]")
250
+ console.print()
251
+
252
+
253
+ def show_launch_context_panel(ctx: LaunchContext) -> None:
254
+ """Display enhanced launch context panel with path information.
255
+
256
+ Shows:
257
+ - Workspace root (WR)
258
+ - Entry dir (ED) with relative path if different from WR
259
+ - Mount root (MR) only if different from WR (worktree expansion)
260
+ - Container workdir (CW)
261
+ - Team / branch / session / mode
262
+ """
263
+ grid = Table.grid(padding=(0, 2))
264
+ grid.add_column(style="dim", no_wrap=True)
265
+ grid.add_column(style="white")
266
+
267
+ # Workspace root (WR)
268
+ grid.add_row("Workspace:", str(ctx.workspace_root))
269
+
270
+ # Entry dir (ED) - show relative if different from WR
271
+ if ctx.entry_dir != ctx.workspace_root:
272
+ rel = ctx.entry_dir_relative
273
+ if rel != ".":
274
+ grid.add_row("Entry dir:", f"{rel} [dim](relative)[/dim]")
275
+
276
+ # Mount root (MR) - only show if different (worktree expansion)
277
+ if ctx.mount_root != ctx.workspace_root:
278
+ grid.add_row("Mount root:", f"{ctx.mount_root} [dim](expanded for worktree)[/dim]")
279
+
280
+ # Container workdir (CW)
281
+ grid.add_row("Container cwd:", ctx.container_workdir)
282
+
283
+ # Team
284
+ grid.add_row("Team:", ctx.team or "standalone")
285
+
286
+ # Branch
287
+ if ctx.branch:
288
+ grid.add_row("Branch:", ctx.branch)
289
+
290
+ # Session
291
+ if ctx.session_name:
292
+ grid.add_row("Session:", ctx.session_name)
293
+
294
+ # Mode
295
+ mode_display = (
296
+ "[green]Resume existing[/green]" if ctx.mode == "resume" else "[cyan]New container[/cyan]"
297
+ )
298
+ grid.add_row("Mode:", mode_display)
299
+
300
+ panel = Panel(
301
+ grid,
302
+ title="[bold green]Launching Claude Code[/bold green]",
303
+ border_style="green",
304
+ padding=(0, 1),
305
+ )
306
+
307
+ console.print()
308
+ console.print(panel)
309
+ console.print()
@@ -0,0 +1,135 @@
1
+ """
2
+ Docker sandbox launching functions.
3
+
4
+ This module handles Docker container creation and execution for launch command:
5
+ - Container creation or resume
6
+ - Session recording
7
+ - Context recording for Quick Resume
8
+ - Docker process handoff
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from ... import config, docker, git, sessions
17
+ from ...contexts import WorkContext, record_context
18
+ from ...output_mode import print_human
19
+ from .render import show_launch_panel
20
+
21
+ if TYPE_CHECKING:
22
+ pass
23
+
24
+
25
+ def launch_sandbox(
26
+ workspace_path: Path | None,
27
+ mount_path: Path | None,
28
+ team: str | None,
29
+ session_name: str | None,
30
+ current_branch: str | None,
31
+ should_continue_session: bool,
32
+ fresh: bool,
33
+ ) -> None:
34
+ """
35
+ Execute the Docker sandbox with all configurations applied.
36
+
37
+ Handles container creation, session recording, and process handoff.
38
+ Safety-net policy from org config is extracted and mounted read-only.
39
+ """
40
+ # Load org config for safety-net policy injection
41
+ # This is already cached by _configure_team_settings(), so it's a fast read
42
+ org_config = config.load_cached_org_config()
43
+
44
+ # Prepare sandbox volume for credential persistence
45
+ docker.prepare_sandbox_volume_for_credentials()
46
+
47
+ # Get or create container
48
+ docker_cmd, is_resume = docker.get_or_create_container(
49
+ workspace=mount_path,
50
+ branch=current_branch,
51
+ profile=team,
52
+ force_new=fresh,
53
+ continue_session=should_continue_session,
54
+ env_vars=None,
55
+ )
56
+
57
+ # Extract container name for session tracking
58
+ container_name = extract_container_name(docker_cmd, is_resume)
59
+
60
+ # Record session and context
61
+ if workspace_path:
62
+ sessions.record_session(
63
+ workspace=str(workspace_path),
64
+ team=team,
65
+ session_name=session_name,
66
+ container_name=container_name,
67
+ branch=current_branch,
68
+ )
69
+ # Record context for quick resume feature
70
+ # Determine repo root (may be same as workspace for non-worktrees)
71
+ repo_root = git.get_worktree_main_repo(workspace_path) or workspace_path
72
+ worktree_name = workspace_path.name
73
+ context = WorkContext(
74
+ team=team, # Keep None for standalone mode (don't use "base")
75
+ repo_root=repo_root,
76
+ worktree_path=workspace_path,
77
+ worktree_name=worktree_name,
78
+ branch=current_branch, # For Quick Resume branch highlighting
79
+ last_session_id=session_name,
80
+ )
81
+ # Context recording is best-effort - failure should never block sandbox launch
82
+ # (Quick Resume is a convenience feature, not critical path)
83
+ try:
84
+ record_context(context)
85
+ except (OSError, ValueError) as e:
86
+ import logging
87
+
88
+ print_human(
89
+ "[yellow]Warning:[/yellow] Could not save Quick Resume context.",
90
+ highlight=False,
91
+ )
92
+ print_human(f"[dim]{e}[/dim]", highlight=False)
93
+ logging.debug(f"Failed to record context for Quick Resume: {e}")
94
+
95
+ if team:
96
+ try:
97
+ config.set_workspace_team(str(workspace_path), team)
98
+ except (OSError, ValueError) as e:
99
+ import logging
100
+
101
+ print_human(
102
+ "[yellow]Warning:[/yellow] Could not save workspace team preference.",
103
+ highlight=False,
104
+ )
105
+ print_human(f"[dim]{e}[/dim]", highlight=False)
106
+ logging.debug(f"Failed to store workspace team mapping: {e}")
107
+
108
+ # Show launch info and execute
109
+ show_launch_panel(
110
+ workspace=workspace_path,
111
+ team=team,
112
+ session_name=session_name,
113
+ branch=current_branch,
114
+ is_resume=is_resume,
115
+ )
116
+
117
+ # Pass org_config for safety-net policy injection (mounted read-only)
118
+ # Pass workspace_path as container_workdir so Claude's CWD is the actual workspace
119
+ # (mount_path may be a parent directory for worktree support)
120
+ docker.run(docker_cmd, org_config=org_config, container_workdir=workspace_path)
121
+
122
+
123
+ def extract_container_name(docker_cmd: list[str], is_resume: bool) -> str | None:
124
+ """Extract container name from docker command for session tracking."""
125
+ for idx, arg in enumerate(docker_cmd):
126
+ if arg == "--name" and idx + 1 < len(docker_cmd):
127
+ return docker_cmd[idx + 1]
128
+ if arg.startswith("--name="):
129
+ return arg.split("=", 1)[1]
130
+
131
+ if is_resume and docker_cmd:
132
+ # For resume, container name is the last arg
133
+ if docker_cmd[-1].startswith("scc-"):
134
+ return docker_cmd[-1]
135
+ return None