steerdev 1.1.3__tar.gz → 1.1.6__tar.gz

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 (116) hide show
  1. {steerdev-1.1.3 → steerdev-1.1.6}/PKG-INFO +1 -1
  2. {steerdev-1.1.3 → steerdev-1.1.6}/pyproject.toml +1 -1
  3. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/cli.py +45 -2
  4. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/runner.py +36 -17
  5. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/claude_setup.py +45 -0
  6. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workflow/executor.py +3 -0
  7. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_claude_setup.py +82 -1
  8. {steerdev-1.1.3 → steerdev-1.1.6}/.github/workflows/pre-commit.yml +0 -0
  9. {steerdev-1.1.3 → steerdev-1.1.6}/.github/workflows/publish.yml +0 -0
  10. {steerdev-1.1.3 → steerdev-1.1.6}/.gitignore +0 -0
  11. {steerdev-1.1.3 → steerdev-1.1.6}/.pre-commit-config.yaml +0 -0
  12. {steerdev-1.1.3 → steerdev-1.1.6}/AGENTS.md +0 -0
  13. {steerdev-1.1.3 → steerdev-1.1.6}/CLAUDE.md +0 -0
  14. {steerdev-1.1.3 → steerdev-1.1.6}/README.md +0 -0
  15. {steerdev-1.1.3 → steerdev-1.1.6}/scripts/pre-commit-version-bump.sh +0 -0
  16. {steerdev-1.1.3 → steerdev-1.1.6}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
  17. {steerdev-1.1.3 → steerdev-1.1.6}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
  18. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/__init__.py +0 -0
  19. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/agent_loop.py +0 -0
  20. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/__init__.py +0 -0
  21. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/activity.py +0 -0
  22. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/agents.py +0 -0
  23. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/canals.py +0 -0
  24. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/client.py +0 -0
  25. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/commands.py +0 -0
  26. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/configs.py +0 -0
  27. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/context.py +0 -0
  28. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/events.py +0 -0
  29. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/hooks.py +0 -0
  30. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/implementation_plan.py +0 -0
  31. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/merger.py +0 -0
  32. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/messages.py +0 -0
  33. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/prd.py +0 -0
  34. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/reports.py +0 -0
  35. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/runs.py +0 -0
  36. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/sessions.py +0 -0
  37. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/specs.py +0 -0
  38. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/tasks.py +0 -0
  39. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/workflow_runs.py +0 -0
  40. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/api/workflows.py +0 -0
  41. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/config/__init__.py +0 -0
  42. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/config/models.py +0 -0
  43. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/config/platform.py +0 -0
  44. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/config/settings.py +0 -0
  45. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/evidence.py +0 -0
  46. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/evidence_assets.py +0 -0
  47. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/executor/__init__.py +0 -0
  48. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/executor/base.py +0 -0
  49. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/executor/claude.py +0 -0
  50. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/executor/stream.py +0 -0
  51. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/handlers/__init__.py +0 -0
  52. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/handlers/prd.py +0 -0
  53. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/integration.py +0 -0
  54. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/prompt/__init__.py +0 -0
  55. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/prompt/builder.py +0 -0
  56. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/prompt/templates.py +0 -0
  57. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/prompt/workflow_template.py +0 -0
  58. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/py.typed +0 -0
  59. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/retry.py +0 -0
  60. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/__init__.py +0 -0
  61. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/repo_setup.py +0 -0
  62. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
  63. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
  64. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/settings.json +0 -0
  65. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
  66. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
  67. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
  68. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
  69. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
  70. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
  71. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
  72. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
  73. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
  74. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
  75. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
  76. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
  77. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/update_check.py +0 -0
  78. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/version.py +0 -0
  79. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workflow/__init__.py +0 -0
  80. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workflow/context.py +0 -0
  81. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workflow/memory.py +0 -0
  82. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workspace/__init__.py +0 -0
  83. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workspace/preparation.py +0 -0
  84. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workspace/project_manager.py +0 -0
  85. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/workspace/tool_detection.py +0 -0
  86. {steerdev-1.1.3 → steerdev-1.1.6}/src/steerdev_agent/worktree.py +0 -0
  87. {steerdev-1.1.3 → steerdev-1.1.6}/tests/__init__.py +0 -0
  88. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_agent_loop.py +0 -0
  89. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_agent_loop_extended.py +0 -0
  90. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_agents_api.py +0 -0
  91. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_api_client.py +0 -0
  92. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_claude_executor.py +0 -0
  93. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_client_methods.py +0 -0
  94. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_commands_api.py +0 -0
  95. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_config.py +0 -0
  96. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_config_extended.py +0 -0
  97. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_conflict_mitigation.py +0 -0
  98. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_context_search.py +0 -0
  99. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_executor.py +0 -0
  100. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_platform_config.py +0 -0
  101. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_preparation.py +0 -0
  102. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_prompt.py +0 -0
  103. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_reports_client.py +0 -0
  104. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_retry.py +0 -0
  105. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_runner_merge_modes.py +0 -0
  106. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_runner_worktrees.py +0 -0
  107. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_stream_parser.py +0 -0
  108. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_tasks.py +0 -0
  109. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_version.py +0 -0
  110. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workflow_context.py +0 -0
  111. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workflow_memory.py +0 -0
  112. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workflow_prompt_template.py +0 -0
  113. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workflow_runs_api.py +0 -0
  114. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workspace.py +0 -0
  115. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_workspace_extended.py +0 -0
  116. {steerdev-1.1.3 → steerdev-1.1.6}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerdev
3
- Version: 1.1.3
3
+ Version: 1.1.6
4
4
  Summary: Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting
5
5
  Project-URL: Homepage, https://github.com/pentoai/steerdev-agent
6
6
  Project-URL: Repository, https://github.com/pentoai/steerdev-agent
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerdev"
3
- version = "1.1.3"
3
+ version = "1.1.6"
4
4
  description = "Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1617,6 +1617,17 @@ def run(
1617
1617
  envvar="STEERDEV_AGENT_NAME",
1618
1618
  ),
1619
1619
  ] = None,
1620
+ agent_id: Annotated[
1621
+ str | None,
1622
+ typer.Option(
1623
+ "--agent-id",
1624
+ help=(
1625
+ "Pre-existing agent ID to attach this run to (skips agent registration)."
1626
+ " Reads from STEERDEV_AGENT_ID env var."
1627
+ ),
1628
+ envvar="STEERDEV_AGENT_ID",
1629
+ ),
1630
+ ] = None,
1620
1631
  model: Annotated[
1621
1632
  str | None,
1622
1633
  typer.Option(
@@ -1726,6 +1737,14 @@ def run(
1726
1737
  help="Default branch for sync/rebase/PR targets (default: from config or 'main')",
1727
1738
  ),
1728
1739
  ] = None,
1740
+ task_id: Annotated[
1741
+ str | None,
1742
+ typer.Option(
1743
+ "--task-id",
1744
+ "-t",
1745
+ help="Run a specific task by ID (bypasses queue/wave scheduling)",
1746
+ ),
1747
+ ] = None,
1729
1748
  ) -> None:
1730
1749
  """Run the agent for a project!
1731
1750
 
@@ -1851,6 +1870,8 @@ def run(
1851
1870
  dry_run=dry_run,
1852
1871
  retry_config=retry_config,
1853
1872
  branch_config=resolved_branch_config,
1873
+ task_id=task_id,
1874
+ agent_id=agent_id,
1854
1875
  )
1855
1876
  )
1856
1877
 
@@ -2341,8 +2362,15 @@ def _display_dependency_check(deps: list) -> None:
2341
2362
 
2342
2363
  for dep in deps:
2343
2364
  if dep.found:
2344
- status = "[green]Found[/green]"
2345
- details = dep.version or dep.path or ""
2365
+ if dep.authenticated is False:
2366
+ status = "[red]Not authenticated[/red]"
2367
+ details = dep.auth_hint or "Run the tool's login command"
2368
+ elif dep.authenticated is True:
2369
+ status = "[green]Found & authenticated[/green]"
2370
+ details = dep.auth_details or dep.version or dep.path or ""
2371
+ else:
2372
+ status = "[green]Found[/green]"
2373
+ details = dep.version or dep.path or ""
2346
2374
  elif dep.required:
2347
2375
  status = "[red]Missing (required)[/red]"
2348
2376
  details = f"Install: {dep.install_hint}"
@@ -2488,6 +2516,21 @@ def setup(
2488
2516
  )
2489
2517
  raise typer.Exit(1)
2490
2518
 
2519
+ unauthenticated_required = [
2520
+ d for d in deps if d.required and d.found and d.authenticated is False
2521
+ ]
2522
+ if unauthenticated_required:
2523
+ names = ", ".join(d.name for d in unauthenticated_required)
2524
+ hints = "\n".join(
2525
+ f" - {d.name}: {d.auth_hint}" for d in unauthenticated_required if d.auth_hint
2526
+ )
2527
+ console.print(
2528
+ f"\n[red]Required tool(s) not authenticated: {names}.[/red]\n"
2529
+ f"Log in before running setup so steerdev can create PRs and "
2530
+ f"manage branches on your behalf.\n{hints}"
2531
+ )
2532
+ raise typer.Exit(1)
2533
+
2491
2534
  # Prompt for install target if not provided
2492
2535
  if install_target is None:
2493
2536
  install_target = _prompt_install_target()
@@ -287,6 +287,7 @@ class Runner:
287
287
  branch_config: BranchConfig | None = None,
288
288
  shutdown_event: asyncio.Event | None = None,
289
289
  agent_id: str | None = None,
290
+ task_id: str | None = None,
290
291
  ) -> None:
291
292
  """Initialize the runner.
292
293
 
@@ -334,6 +335,7 @@ class Runner:
334
335
  self._enable_waves = enable_waves
335
336
  self._enable_canals = enable_canals
336
337
  self._agent_id = agent_id
338
+ self._target_task_id = task_id
337
339
 
338
340
  # State
339
341
  self._state = RunState.STOPPED
@@ -424,6 +426,7 @@ class Runner:
424
426
  request = SessionCreateRequest(
425
427
  project_id=self.project_id,
426
428
  task_id=task_id,
429
+ agent_id=self._agent_id,
427
430
  agent_name=self.agent_name,
428
431
  agent_type=self.agent_type,
429
432
  prompt=prompt,
@@ -1021,6 +1024,7 @@ class Runner:
1021
1024
  project_id=self.project_id,
1022
1025
  agent_type=self.agent_type,
1023
1026
  agent_name=self.agent_name,
1027
+ agent_id=self._agent_id,
1024
1028
  shutdown_event=self._shutdown_event,
1025
1029
  )
1026
1030
 
@@ -1124,25 +1128,36 @@ class Runner:
1124
1128
  suggested_workflow_id: str | None = None
1125
1129
 
1126
1130
  with TasksClient(api_key=self._api_key) as client:
1127
- if self._enable_waves:
1128
- try:
1129
- task, wave_ctx, suggested_workflow_id = extract_task_and_wave_context(
1130
- client.get_next_wave(
1131
- project_id=self.project_id,
1132
- agent_id=self._agent_id,
1131
+ if self._target_task_id:
1132
+ if self._tasks_executed > 0:
1133
+ # Targeted single-task mode: only run the requested task once.
1134
+ break
1135
+ task = client.get_task(self._target_task_id)
1136
+ if not task:
1137
+ console.print(f"[red]Task {self._target_task_id} not found[/red]")
1138
+ break
1139
+ else:
1140
+ if self._enable_waves:
1141
+ try:
1142
+ task, wave_ctx, suggested_workflow_id = (
1143
+ extract_task_and_wave_context(
1144
+ client.get_next_wave(
1145
+ project_id=self.project_id,
1146
+ agent_id=self._agent_id,
1147
+ )
1148
+ )
1149
+ )
1150
+ except Exception:
1151
+ logger.debug(
1152
+ "Wave fetch failed, falling back to single-task",
1153
+ exc_info=True,
1133
1154
  )
1134
- )
1135
- except Exception:
1136
- logger.debug(
1137
- "Wave fetch failed, falling back to single-task",
1138
- exc_info=True,
1139
- )
1140
1155
 
1141
- if not task:
1142
- task = client.get_next_task(
1143
- project_id=self.project_id,
1144
- agent_id=self._agent_id,
1145
- )
1156
+ if not task:
1157
+ task = client.get_next_task(
1158
+ project_id=self.project_id,
1159
+ agent_id=self._agent_id,
1160
+ )
1146
1161
 
1147
1162
  if not task:
1148
1163
  if self._tasks_executed == 0:
@@ -1395,6 +1410,8 @@ async def run_agent(
1395
1410
  dry_run: bool = False,
1396
1411
  retry_config: RetryConfig | None = None,
1397
1412
  branch_config: BranchConfig | None = None,
1413
+ task_id: str | None = None,
1414
+ agent_id: str | None = None,
1398
1415
  ) -> dict[str, Any]:
1399
1416
  """Run the steerdev agent.
1400
1417
 
@@ -1442,6 +1459,8 @@ async def run_agent(
1442
1459
  dry_run=dry_run,
1443
1460
  retry_config=retry_config,
1444
1461
  branch_config=branch_config,
1462
+ task_id=task_id,
1463
+ agent_id=agent_id,
1445
1464
  )
1446
1465
 
1447
1466
  # Install signal handler that kills the subprocess immediately on Ctrl+C
@@ -30,6 +30,11 @@ class DependencyStatus:
30
30
  version: str | None = None
31
31
  required: bool = True
32
32
  install_hint: str = ""
33
+ # Auth status: True if authenticated, False if not, None if not applicable
34
+ # (or the binary was not found, or the check could not be performed).
35
+ authenticated: bool | None = None
36
+ auth_hint: str = ""
37
+ auth_details: str | None = None
33
38
 
34
39
 
35
40
  def check_cli_dependencies() -> list[DependencyStatus]:
@@ -75,6 +80,15 @@ def check_cli_dependencies() -> list[DependencyStatus]:
75
80
  install_hint="https://nodejs.org/",
76
81
  ),
77
82
  ]
83
+
84
+ for dep in deps:
85
+ if dep.command == "gh" and dep.found and dep.path is not None:
86
+ authenticated, details = _check_gh_auth(dep.path)
87
+ dep.authenticated = authenticated
88
+ dep.auth_details = details
89
+ if authenticated is False:
90
+ dep.auth_hint = "Run: gh auth login"
91
+
78
92
  return deps
79
93
 
80
94
 
@@ -125,6 +139,37 @@ def _check_dep(
125
139
  )
126
140
 
127
141
 
142
+ def _check_gh_auth(gh_path: str) -> tuple[bool | None, str | None]:
143
+ """Check whether the GitHub CLI is logged in.
144
+
145
+ Returns a tuple of (authenticated, details). `authenticated` is True when
146
+ `gh auth status` exits 0, False when it exits non-zero (meaning gh is
147
+ installed but the user isn't logged in), and None when the check could
148
+ not be executed (timeout / OS error).
149
+ """
150
+ try:
151
+ result = subprocess.run(
152
+ [gh_path, "auth", "status"],
153
+ capture_output=True,
154
+ text=True,
155
+ timeout=5,
156
+ )
157
+ except (subprocess.TimeoutExpired, OSError):
158
+ logger.debug("Failed to run `gh auth status`")
159
+ return None, None
160
+
161
+ # `gh auth status` writes its human-readable output to stderr.
162
+ combined = (result.stderr or "") + (result.stdout or "")
163
+ details: str | None = None
164
+ for line in combined.splitlines():
165
+ stripped = line.strip()
166
+ if "Logged in to" in stripped or "not logged in" in stripped.lower():
167
+ details = stripped
168
+ break
169
+
170
+ return result.returncode == 0, details
171
+
172
+
128
173
  # Word lists for generating agent names
129
174
  ADJECTIVES = [
130
175
  "bright",
@@ -50,6 +50,7 @@ class WorkflowExecutor:
50
50
  project_id: str | None = None,
51
51
  agent_type: str = "claude",
52
52
  agent_name: str | None = None,
53
+ agent_id: str | None = None,
53
54
  shutdown_event: asyncio.Event | None = None,
54
55
  ) -> None:
55
56
  """Initialize the workflow executor.
@@ -75,6 +76,7 @@ class WorkflowExecutor:
75
76
  self.project_id = project_id
76
77
  self.agent_type = agent_type
77
78
  self.agent_name = agent_name
79
+ self.agent_id = agent_id
78
80
  self._shutdown_event = shutdown_event
79
81
 
80
82
  self._memory_manager = WorkflowMemoryManager(working_directory)
@@ -251,6 +253,7 @@ Complete this phase according to its type: {phase.phase_type}
251
253
  request = SessionCreateRequest(
252
254
  project_id=self.project_id,
253
255
  task_id=task_id,
256
+ agent_id=self.agent_id,
254
257
  agent_name=self.agent_name,
255
258
  agent_type=self.agent_type,
256
259
  prompt=prompt,
@@ -1,6 +1,7 @@
1
1
  """Tests for Claude setup templates and installed skills."""
2
2
 
3
- from unittest.mock import patch
3
+ import subprocess
4
+ from unittest.mock import MagicMock, patch
4
5
 
5
6
  from steerdev_agent.setup.claude_setup import ClaudeSetup, check_cli_dependencies
6
7
 
@@ -86,3 +87,83 @@ class TestCheckCliDependencies:
86
87
  if git_dep.found:
87
88
  assert git_dep.path is not None
88
89
  assert git_dep.version is not None
90
+
91
+
92
+ class TestGhAuthStatus:
93
+ """Tests for gh auth status checking within check_cli_dependencies()."""
94
+
95
+ @staticmethod
96
+ def _fake_run(*, auth_returncode: int, auth_stderr: str = ""):
97
+ """Build a fake subprocess.run that returns a version then an auth result."""
98
+
99
+ def run(cmd, *args, **kwargs):
100
+ # First subprocess call is `--version`; second is `auth status`.
101
+ if "auth" in cmd:
102
+ return MagicMock(returncode=auth_returncode, stdout="", stderr=auth_stderr)
103
+ return MagicMock(returncode=0, stdout="gh version 2.0.0\n", stderr="")
104
+
105
+ return run
106
+
107
+ def test_gh_authenticated_sets_flag_true(self):
108
+ with (
109
+ patch("shutil.which", return_value="/usr/bin/gh"),
110
+ patch(
111
+ "subprocess.run",
112
+ side_effect=self._fake_run(
113
+ auth_returncode=0,
114
+ auth_stderr="✓ Logged in to github.com as octocat",
115
+ ),
116
+ ),
117
+ ):
118
+ deps = check_cli_dependencies()
119
+
120
+ gh_dep = next(d for d in deps if d.command == "gh")
121
+ assert gh_dep.found is True
122
+ assert gh_dep.authenticated is True
123
+ assert gh_dep.auth_hint == ""
124
+ assert gh_dep.auth_details is not None
125
+ assert "Logged in" in gh_dep.auth_details
126
+
127
+ def test_gh_not_authenticated_sets_flag_false_with_hint(self):
128
+ with (
129
+ patch("shutil.which", return_value="/usr/bin/gh"),
130
+ patch(
131
+ "subprocess.run",
132
+ side_effect=self._fake_run(
133
+ auth_returncode=1,
134
+ auth_stderr="You are not logged into any GitHub hosts.",
135
+ ),
136
+ ),
137
+ ):
138
+ deps = check_cli_dependencies()
139
+
140
+ gh_dep = next(d for d in deps if d.command == "gh")
141
+ assert gh_dep.found is True
142
+ assert gh_dep.authenticated is False
143
+ assert "gh auth login" in gh_dep.auth_hint
144
+
145
+ def test_gh_auth_check_timeout_leaves_status_none(self):
146
+ def run(cmd, *args, **kwargs):
147
+ if "auth" in cmd:
148
+ raise subprocess.TimeoutExpired(cmd=cmd, timeout=5)
149
+ return MagicMock(returncode=0, stdout="gh version 2.0.0\n", stderr="")
150
+
151
+ with (
152
+ patch("shutil.which", return_value="/usr/bin/gh"),
153
+ patch("subprocess.run", side_effect=run),
154
+ ):
155
+ deps = check_cli_dependencies()
156
+
157
+ gh_dep = next(d for d in deps if d.command == "gh")
158
+ assert gh_dep.found is True
159
+ assert gh_dep.authenticated is None
160
+ assert gh_dep.auth_hint == ""
161
+
162
+ def test_missing_gh_does_not_run_auth_check(self):
163
+ # When gh isn't installed, authenticated should stay None (not False).
164
+ with patch("shutil.which", return_value=None):
165
+ deps = check_cli_dependencies()
166
+
167
+ gh_dep = next(d for d in deps if d.command == "gh")
168
+ assert gh_dep.found is False
169
+ assert gh_dep.authenticated is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes