@team-agent/installer 0.1.10 → 0.2.0

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 (111) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/skills/team-agent/SKILL.md +1 -1
  5. package/src/team_agent/approvals/__init__.py +65 -0
  6. package/src/team_agent/approvals/constants.py +6 -0
  7. package/src/team_agent/approvals/parsing.py +176 -0
  8. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  9. package/src/team_agent/approvals/status.py +165 -0
  10. package/src/team_agent/cli/__init__.py +135 -0
  11. package/src/team_agent/cli/commands.py +335 -0
  12. package/src/team_agent/cli/e2e.py +202 -0
  13. package/src/team_agent/cli/helpers.py +137 -0
  14. package/src/team_agent/cli/parser.py +470 -0
  15. package/src/team_agent/compiler.py +98 -33
  16. package/src/team_agent/coordinator/__init__.py +53 -0
  17. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  18. package/src/team_agent/coordinator/lifecycle.py +319 -0
  19. package/src/team_agent/coordinator/metadata.py +61 -0
  20. package/src/team_agent/coordinator/paths.py +17 -0
  21. package/src/team_agent/diagnose/__init__.py +48 -0
  22. package/src/team_agent/diagnose/checks.py +101 -0
  23. package/src/team_agent/diagnose/health.py +241 -0
  24. package/src/team_agent/diagnose/preflight.py +194 -0
  25. package/src/team_agent/diagnose/quick_start.py +233 -0
  26. package/src/team_agent/display/__init__.py +61 -0
  27. package/src/team_agent/display/close.py +147 -0
  28. package/src/team_agent/display/ghostty.py +77 -0
  29. package/src/team_agent/display/worker_window.py +110 -0
  30. package/src/team_agent/display/workspace.py +473 -0
  31. package/src/team_agent/launch/__init__.py +41 -0
  32. package/src/team_agent/launch/bootstrap.py +85 -0
  33. package/src/team_agent/launch/config.py +106 -0
  34. package/src/team_agent/launch/core.py +291 -0
  35. package/src/team_agent/launch/requirements.py +57 -0
  36. package/src/team_agent/leader/__init__.py +320 -0
  37. package/src/team_agent/lifecycle/__init__.py +5 -0
  38. package/src/team_agent/lifecycle/agents.py +226 -0
  39. package/src/team_agent/lifecycle/operations.py +321 -0
  40. package/src/team_agent/lifecycle/start.py +360 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +128 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +217 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/result_delivery.py +300 -0
  61. package/src/team_agent/messaging/results.py +456 -0
  62. package/src/team_agent/messaging/scheduler.py +418 -0
  63. package/src/team_agent/messaging/send.py +493 -0
  64. package/src/team_agent/messaging/tmux_io.py +337 -0
  65. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  66. package/src/team_agent/orchestrator/__init__.py +376 -0
  67. package/src/team_agent/orchestrator/plan.py +122 -0
  68. package/src/team_agent/orchestrator/state.py +128 -0
  69. package/src/team_agent/profiles/__init__.py +82 -0
  70. package/src/team_agent/profiles/constants.py +19 -0
  71. package/src/team_agent/profiles/core.py +407 -0
  72. package/src/team_agent/profiles/helpers.py +69 -0
  73. package/src/team_agent/profiles/provider_env.py +188 -0
  74. package/src/team_agent/profiles/smoke.py +201 -0
  75. package/src/team_agent/provider_cli/__init__.py +43 -0
  76. package/src/team_agent/provider_cli/adapter.py +167 -0
  77. package/src/team_agent/provider_cli/base.py +48 -0
  78. package/src/team_agent/provider_cli/claude.py +457 -0
  79. package/src/team_agent/provider_cli/codex.py +319 -0
  80. package/src/team_agent/provider_cli/copilot.py +8 -0
  81. package/src/team_agent/provider_cli/fake.py +39 -0
  82. package/src/team_agent/provider_cli/gemini.py +95 -0
  83. package/src/team_agent/provider_cli/opencode.py +8 -0
  84. package/src/team_agent/provider_cli/prompt.py +62 -0
  85. package/src/team_agent/provider_cli/registry.py +18 -0
  86. package/src/team_agent/provider_cli/unsupported.py +32 -0
  87. package/src/team_agent/providers.py +67 -949
  88. package/src/team_agent/quality_gates.py +104 -0
  89. package/src/team_agent/restart/__init__.py +34 -0
  90. package/src/team_agent/restart/orchestration.py +328 -0
  91. package/src/team_agent/restart/selection.py +89 -0
  92. package/src/team_agent/restart/snapshot.py +70 -0
  93. package/src/team_agent/runtime.py +802 -5740
  94. package/src/team_agent/rust_core.py +22 -5
  95. package/src/team_agent/sessions/__init__.py +25 -0
  96. package/src/team_agent/sessions/capture.py +93 -0
  97. package/src/team_agent/sessions/inventory.py +44 -0
  98. package/src/team_agent/sessions/resume.py +135 -0
  99. package/src/team_agent/spec.py +3 -1
  100. package/src/team_agent/state.py +204 -4
  101. package/src/team_agent/status/__init__.py +63 -0
  102. package/src/team_agent/status/approvals.py +52 -0
  103. package/src/team_agent/status/compact.py +158 -0
  104. package/src/team_agent/status/constants.py +18 -0
  105. package/src/team_agent/status/inbox.py +28 -0
  106. package/src/team_agent/status/peek.py +117 -0
  107. package/src/team_agent/status/queries.py +168 -0
  108. package/src/team_agent/terminal.py +57 -0
  109. package/src/team_agent/cli.py +0 -857
  110. package/src/team_agent/mcp_server.py +0 -579
  111. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from team_agent import profiles
8
+ from team_agent import runtime
9
+ from team_agent.errors import TeamAgentError
10
+
11
+ from team_agent.cli.commands import (
12
+ cmd_quick_start,
13
+ cmd_codex,
14
+ cmd_claude,
15
+ cmd_init,
16
+ cmd_validate,
17
+ cmd_compile,
18
+ cmd_profile_init,
19
+ cmd_profile_doctor,
20
+ cmd_profile_show,
21
+ cmd_launch,
22
+ cmd_preflight,
23
+ cmd_start,
24
+ cmd_wait_ready,
25
+ cmd_settle,
26
+ cmd_status,
27
+ cmd_approvals,
28
+ cmd_peek,
29
+ cmd_inbox,
30
+ cmd_sessions,
31
+ cmd_attach_leader,
32
+ cmd_takeover,
33
+ cmd_send,
34
+ cmd_collect,
35
+ cmd_diagnose,
36
+ cmd_repair_state,
37
+ cmd_validate_result,
38
+ cmd_doctor,
39
+ cmd_shutdown,
40
+ cmd_restart,
41
+ cmd_start_agent,
42
+ cmd_stop_agent,
43
+ cmd_reset_agent,
44
+ cmd_add_agent,
45
+ cmd_fork_agent,
46
+ cmd_remove_agent,
47
+ cmd_stuck_list,
48
+ cmd_stuck_cancel,
49
+ cmd_allow_peer_talk,
50
+ cmd_advanced,
51
+ cmd_install_skill,
52
+ cmd_run_overnight,
53
+
54
+ )
55
+ from team_agent.cli.e2e import cmd_e2e
56
+ from team_agent.cli.helpers import (
57
+ _cli_error_payload,
58
+ _emit_cli_error,
59
+ _leader_launcher_args,
60
+ _provider_args,
61
+ emit,
62
+ )
63
+
64
+
65
+ SEND_ORDER_HINT = (
66
+ "options must appear before target/message. Use: "
67
+ "team-agent send --task <task_id> --json \"<message>\" or "
68
+ "team-agent send --no-ack --json <agent_id> \"<message>\""
69
+ )
70
+
71
+
72
+ class TeamAgentArgumentParser(argparse.ArgumentParser):
73
+ def error(self, message: str) -> None:
74
+ send_command = "send" in sys.argv[1:]
75
+ if (getattr(self, "send_order_hint", False) or send_command) and "unrecognized arguments" in message:
76
+ message = f"{message}\nHint: {SEND_ORDER_HINT}"
77
+ super().error(message)
78
+
79
+
80
+ def main(argv: list[str] | None = None) -> None:
81
+ raw_argv = list(sys.argv[1:] if argv is None else argv)
82
+ if raw_argv and raw_argv[0] in {"codex", "claude"}:
83
+ _run_leader_passthrough(raw_argv[0], raw_argv[1:])
84
+ return
85
+
86
+ parser = TeamAgentArgumentParser(
87
+ prog="team-agent",
88
+ description="TeamSpec Agent Mode CLI",
89
+ epilog="See `team-agent advanced --help` for low-level commands (debugging only).",
90
+ )
91
+ sub = parser.add_subparsers(dest="command", required=True, parser_class=TeamAgentArgumentParser)
92
+
93
+ p = sub.add_parser("codex", help="Start a tmux-managed Codex leader in the current directory")
94
+ p.add_argument("provider_args", nargs=argparse.REMAINDER, help="Arguments passed through to codex")
95
+ p.set_defaults(func=cmd_codex)
96
+
97
+ p = sub.add_parser("claude", help="Start a tmux-managed Claude leader in the current directory")
98
+ p.add_argument("provider_args", nargs=argparse.REMAINDER, help="Arguments passed through to claude")
99
+ p.set_defaults(func=cmd_claude)
100
+
101
+ p = sub.add_parser("quick-start", help="Start a team from a role-doc directory")
102
+ p.add_argument("agents_dir")
103
+ p.add_argument("--name")
104
+ p.add_argument("--team-id", help="Store loose role docs under .team/<team-id> instead of .team/current")
105
+ p.add_argument("--yes", action="store_true")
106
+ p.add_argument("--fresh", action="store_true", help="Start fresh worker sessions even when prior runtime state exists")
107
+ add_json(p)
108
+ p.set_defaults(func=cmd_quick_start)
109
+
110
+ p = sub.add_parser("init", help=argparse.SUPPRESS)
111
+ p.add_argument("--workspace", default=".")
112
+ p.add_argument("--force", action="store_true")
113
+ add_json(p)
114
+ p.set_defaults(func=cmd_init)
115
+
116
+ p = sub.add_parser("validate", help=argparse.SUPPRESS)
117
+ p.add_argument("spec", nargs="?", default="team.spec.yaml")
118
+ add_json(p)
119
+ p.set_defaults(func=cmd_validate)
120
+
121
+ p = sub.add_parser("compile", help=argparse.SUPPRESS)
122
+ p.add_argument("--team", required=True, help="Team doc directory, for example .team/current")
123
+ p.add_argument("--out", default="team.spec.yaml")
124
+ add_json(p)
125
+ p.set_defaults(func=cmd_compile)
126
+
127
+ p = sub.add_parser("profile", help=argparse.SUPPRESS)
128
+ profile_sub = p.add_subparsers(dest="profile_command", required=True)
129
+ p_init = profile_sub.add_parser("init", help="Create an example profile template without real secrets")
130
+ p_init.add_argument("name")
131
+ p_init.add_argument("--workspace", default=".")
132
+ p_init.add_argument("--team", help="Team directory whose profiles/ directory should be used")
133
+ p_init.add_argument("--auth-mode", required=True, choices=sorted(profiles.AUTH_MODES))
134
+ add_json(p_init)
135
+ p_init.set_defaults(func=cmd_profile_init)
136
+ p_doctor = profile_sub.add_parser("doctor", help="Check whether a profile exists without printing secrets")
137
+ p_doctor.add_argument("name")
138
+ p_doctor.add_argument("--workspace", default=".")
139
+ p_doctor.add_argument("--team", help="Team directory whose profiles/ directory should be used")
140
+ add_json(p_doctor)
141
+ p_doctor.set_defaults(func=cmd_profile_doctor)
142
+ p_show = profile_sub.add_parser("show", help="Show redacted profile status without printing secrets")
143
+ p_show.add_argument("name")
144
+ p_show.add_argument("--workspace", default=".")
145
+ p_show.add_argument("--team", help="Team directory whose profiles/ directory should be used")
146
+ add_json(p_show)
147
+ p_show.set_defaults(func=cmd_profile_show)
148
+
149
+ p = sub.add_parser("launch", help=argparse.SUPPRESS)
150
+ p.add_argument("spec", nargs="?", default="team.spec.yaml")
151
+ p.add_argument("--yes", action="store_true", help="Confirm launch after permission summary review")
152
+ p.add_argument("--dry-run", action="store_true")
153
+ add_json(p)
154
+ p.set_defaults(func=cmd_launch)
155
+
156
+ p = sub.add_parser("preflight", help=argparse.SUPPRESS)
157
+ p.add_argument("--team", required=True)
158
+ add_json(p)
159
+ p.set_defaults(func=cmd_preflight)
160
+
161
+ p = sub.add_parser("start", help=argparse.SUPPRESS)
162
+ p.add_argument("--team", required=True)
163
+ p.add_argument("--yes", action="store_true")
164
+ add_json(p)
165
+ p.set_defaults(func=cmd_start)
166
+
167
+ p = sub.add_parser("wait-ready", help=argparse.SUPPRESS)
168
+ p.add_argument("--workspace", default=".")
169
+ p.add_argument("--timeout", type=int, default=120)
170
+ add_json(p)
171
+ p.set_defaults(func=cmd_wait_ready)
172
+
173
+ p = sub.add_parser("settle", help=argparse.SUPPRESS)
174
+ p.add_argument("--workspace", default=".")
175
+ add_json(p)
176
+ p.set_defaults(func=cmd_settle)
177
+
178
+ p = sub.add_parser("status", help="Show team runtime status")
179
+ p.add_argument("agent", nargs="?")
180
+ p.add_argument("--workspace", default=".")
181
+ p.add_argument("--detail", action="store_true", help="Include full raw runtime state in --json output")
182
+ add_json(p)
183
+ p.set_defaults(func=cmd_status)
184
+
185
+ p = sub.add_parser("approvals", help="Show structured pending worker approval prompts")
186
+ p.add_argument("agent", nargs="?")
187
+ p.add_argument("--workspace", default=".")
188
+ add_json(p)
189
+ p.set_defaults(func=cmd_approvals)
190
+
191
+ p = sub.add_parser("peek", help=argparse.SUPPRESS, description="Explicit raw-screen diagnostic only")
192
+ p.add_argument("agent")
193
+ p.add_argument("--workspace", default=".")
194
+ p.add_argument(
195
+ "--allow-raw-screen",
196
+ action="store_true",
197
+ help="Required after explicit user authorization to capture worker terminal output",
198
+ )
199
+ mode = p.add_mutually_exclusive_group(required=True)
200
+ mode.add_argument("--head", type=int, help="Show the first N lines from the bounded recent capture")
201
+ mode.add_argument("--tail", type=int, help="Show the last N lines")
202
+ mode.add_argument("--search", help="Search the bounded recent capture and show matching context only")
203
+ p.add_argument("--context", type=int, default=3, help="Context lines around --search matches, max 10")
204
+ add_json(p)
205
+ p.set_defaults(func=cmd_peek)
206
+
207
+ p = sub.add_parser("inbox", help="Show message history for one agent")
208
+ p.add_argument("agent")
209
+ p.add_argument("--workspace", default=".")
210
+ p.add_argument("--limit", type=int, default=20)
211
+ add_json(p)
212
+ p.set_defaults(func=cmd_inbox)
213
+
214
+ p = sub.add_parser("sessions", help=argparse.SUPPRESS)
215
+ p.add_argument("--workspace", default=".")
216
+ add_json(p)
217
+ p.set_defaults(func=cmd_sessions)
218
+
219
+ p = sub.add_parser("attach-leader", help=argparse.SUPPRESS)
220
+ p.add_argument("--workspace", default=".")
221
+ p.add_argument("--pane", help="Explicit tmux pane id or target, for example %%173")
222
+ p.add_argument("--provider", default="codex")
223
+ add_json(p)
224
+ p.set_defaults(func=cmd_attach_leader)
225
+
226
+ p = sub.add_parser("takeover", help="Claim ownership of an existing team (multi-leader same workspace)")
227
+ p.add_argument("--workspace", default=".")
228
+ p.add_argument("--team", help="Explicit team/session selector when a workspace has multiple teams")
229
+ p.add_argument("--confirm", action="store_true", help="Required: confirm you intend to overwrite the recorded team_owner")
230
+ add_json(p)
231
+ p.set_defaults(func=cmd_takeover)
232
+
233
+ p = sub.add_parser(
234
+ "send",
235
+ help="Send a message to an agent, task assignee, or attached leader",
236
+ epilog=(
237
+ "Canonical examples:\n"
238
+ " team-agent send --task <task_id> --json \"<message>\"\n"
239
+ " team-agent send --no-ack --json <agent_id> \"<message>\""
240
+ ),
241
+ formatter_class=argparse.RawDescriptionHelpFormatter,
242
+ )
243
+ p.send_order_hint = True
244
+ p.add_argument("target", nargs="?")
245
+ p.add_argument("message", nargs="+")
246
+ p.add_argument("--to", dest="targets", help="Comma-separated fan-out recipients, for example agent_a,agent_b")
247
+ p.add_argument("--workspace", default=".")
248
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
249
+ p.add_argument("--task")
250
+ p.add_argument("--from", dest="sender", default="leader")
251
+ p.add_argument("--no-ack", action="store_true")
252
+ p.add_argument("--no-wait", action="store_true", help="Return after injection without visible verification")
253
+ p.add_argument(
254
+ "--watch-result",
255
+ action="store_true",
256
+ help="Return after delivery and let the coordinator collect/report the task result asynchronously",
257
+ )
258
+ p.add_argument("--timeout", type=float, default=30.0)
259
+ p.add_argument("--confirm-human", action="store_true", help="Confirm dispatch for a task marked human_confirmation: true")
260
+ add_json(p)
261
+ p.set_defaults(func=cmd_send)
262
+
263
+ p = sub.add_parser("collect", help=argparse.SUPPRESS)
264
+ p.add_argument("--workspace", default=".")
265
+ p.add_argument("--result-file")
266
+ add_json(p)
267
+ p.set_defaults(func=cmd_collect)
268
+
269
+ p = sub.add_parser("diagnose", help=argparse.SUPPRESS)
270
+ p.add_argument("--workspace", default=".")
271
+ add_json(p)
272
+ p.set_defaults(func=cmd_diagnose)
273
+
274
+ p = sub.add_parser("repair-state", help=argparse.SUPPRESS)
275
+ p.add_argument("--workspace", default=".")
276
+ p.add_argument("--task", required=True)
277
+ p.add_argument("--assignee")
278
+ p.add_argument("--status")
279
+ p.add_argument("--summary")
280
+ add_json(p)
281
+ p.set_defaults(func=cmd_repair_state)
282
+
283
+ p = sub.add_parser("validate-result", help=argparse.SUPPRESS)
284
+ p.add_argument("result", nargs="?", help="JSON string. If omitted, read stdin.")
285
+ p.add_argument("--file", help="Read JSON envelope from a file")
286
+ add_json(p)
287
+ p.set_defaults(func=cmd_validate_result)
288
+
289
+ p = sub.add_parser("doctor", help="Check local dependencies, providers, auth hints, tmux, and MCP")
290
+ p.add_argument("spec", nargs="?")
291
+ add_json(p)
292
+ p.set_defaults(func=cmd_doctor)
293
+
294
+ p = sub.add_parser("shutdown", help="Shutdown team tmux session and keep logs")
295
+ p.add_argument("--workspace", default=".")
296
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
297
+ p.add_argument("--keep-logs", action="store_true", default=True)
298
+ add_json(p)
299
+ p.set_defaults(func=cmd_shutdown)
300
+
301
+ p = sub.add_parser("restart", help="Restart a stopped team from stored worker sessions")
302
+ p.add_argument("workspace", nargs="?", default=".")
303
+ p.add_argument("--team", help="Restart a specific stored team/session when the workspace has multiple teams")
304
+ p.add_argument("--allow-fresh", action="store_true", help="Allow fresh worker sessions if stored sessions cannot resume")
305
+ add_json(p)
306
+ p.set_defaults(func=cmd_restart)
307
+
308
+ p = sub.add_parser("start-agent", help="Start or repair one worker in the current team")
309
+ p.add_argument("agent")
310
+ p.add_argument("--workspace", default=".")
311
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
312
+ p.add_argument("--force", action="store_true", help="Replace an existing tmux window for this worker")
313
+ p.add_argument("--allow-fresh", action="store_true", help="Allow a fresh session if the stored session cannot resume")
314
+ p.add_argument("--no-display", action="store_true", help="Do not open a Ghostty display window")
315
+ add_json(p)
316
+ p.set_defaults(func=cmd_start_agent)
317
+
318
+ p = sub.add_parser("stop-agent", help="Hard-stop one running worker while preserving its session for start-agent")
319
+ p.add_argument("agent")
320
+ p.add_argument("--workspace", default=".")
321
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
322
+ add_json(p)
323
+ p.set_defaults(func=cmd_stop_agent)
324
+
325
+ p = sub.add_parser("reset-agent", help="Reset one worker to a fresh session after explicit confirmation")
326
+ p.add_argument("agent")
327
+ p.add_argument("--workspace", default=".")
328
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
329
+ p.add_argument("--discard-session", action="store_true", help="Required: discard this worker's prior provider session")
330
+ p.add_argument("--no-display", action="store_true", help="Do not update a Ghostty display window")
331
+ add_json(p)
332
+ p.set_defaults(func=cmd_reset_agent)
333
+
334
+ p = sub.add_parser("add-agent", help="Add a first-class worker from an explicit workspace-relative role file")
335
+ p.add_argument("agent")
336
+ p.add_argument("--workspace", default=".")
337
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
338
+ p.add_argument("--role-file", required=True, help="Workspace-relative YAML/Markdown agent entry")
339
+ p.add_argument("--no-display", action="store_true", help="Do not open a Ghostty display window")
340
+ add_json(p)
341
+ p.set_defaults(func=cmd_add_agent)
342
+
343
+ p = sub.add_parser("fork-agent", help="Fork a running worker using the provider's native branch/fork support")
344
+ p.add_argument("source_agent")
345
+ p.add_argument("--workspace", default=".")
346
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
347
+ p.add_argument("--as", dest="as_agent", required=True, help="New worker agent id")
348
+ p.add_argument("--label", help="Optional audit label")
349
+ p.add_argument("--no-display", action="store_true", help="Do not open a Ghostty display window")
350
+ add_json(p)
351
+ p.set_defaults(func=cmd_fork_agent)
352
+
353
+ p = sub.add_parser("remove-agent", help="Remove one worker from runtime state and team spec")
354
+ p.add_argument("agent")
355
+ p.add_argument("--workspace", default=".")
356
+ p.add_argument("--team", help="Explicit team/session target when a workspace has multiple teams")
357
+ p.add_argument("--from-spec", action="store_true", help="Allow removing a spec-native worker")
358
+ p.add_argument("--confirm", action="store_true", help="Required with --from-spec")
359
+ p.add_argument("--force", action="store_true", help="Stop a running worker before removing it")
360
+ add_json(p)
361
+ p.set_defaults(func=cmd_remove_agent)
362
+
363
+ p = sub.add_parser("stuck-list", help="List manually suppressed idle-triggered alerts")
364
+ p.add_argument("--workspace", default=".")
365
+ add_json(p)
366
+ p.set_defaults(func=cmd_stuck_list)
367
+
368
+ p = sub.add_parser("stuck-cancel", help="Suppress repeated stuck/idle alerts for one agent")
369
+ p.add_argument("agent")
370
+ p.add_argument("--workspace", default=".")
371
+ p.add_argument("--alert-type", choices=["stuck", "idle_fallback", "all"], default="stuck")
372
+ add_json(p)
373
+ p.set_defaults(func=cmd_stuck_cancel)
374
+
375
+ p = sub.add_parser("install-skill", help=argparse.SUPPRESS)
376
+ p.add_argument("--target", choices=["codex", "claude", "all"], default="codex")
377
+ p.add_argument("--dest", help="Explicit destination directory; overrides --target")
378
+ p.add_argument("--dry-run", action="store_true")
379
+ add_json(p)
380
+ p.set_defaults(func=cmd_install_skill)
381
+
382
+ p = sub.add_parser("e2e", help=argparse.SUPPRESS)
383
+ p.add_argument("--providers", default="fake")
384
+ p.add_argument("--workspace")
385
+ p.add_argument("--real", action="store_true", help="Launch real provider CLIs; may use authenticated accounts")
386
+ add_json(p)
387
+ p.set_defaults(func=cmd_e2e)
388
+
389
+ p = sub.add_parser("allow-peer-talk", help=argparse.SUPPRESS)
390
+ p.add_argument("agent_a")
391
+ p.add_argument("agent_b")
392
+ p.add_argument("--workspace", default=".")
393
+ add_json(p)
394
+ p.set_defaults(func=cmd_allow_peer_talk)
395
+
396
+ p = sub.add_parser(
397
+ "run-overnight",
398
+ help="Run an orchestrator plan that advances on report_result and halts on failure",
399
+ )
400
+ p.add_argument("--workspace", default=".")
401
+ p.add_argument("--plan", help="Path to plan YAML (required unless --status without --plan-id)")
402
+ p.add_argument("--no-start", action="store_true", help="Persist plan state without dispatching stage 1 (default: dispatch)")
403
+ p.add_argument("--status", action="store_true", help="Show plan status instead of starting")
404
+ p.add_argument("--halt", action="store_true", help="Halt the plan named by --plan-id")
405
+ p.add_argument("--plan-id", help="Plan id for --status or --halt")
406
+ p.add_argument("--reason", default="user_requested", help="Halt reason for --halt")
407
+ add_json(p)
408
+ p.set_defaults(func=cmd_run_overnight)
409
+
410
+ p = sub.add_parser(
411
+ "advanced",
412
+ help=argparse.SUPPRESS,
413
+ description="Low-level Team Agent commands",
414
+ epilog=(
415
+ "Commands: init validate compile profile launch preflight start wait-ready settle "
416
+ "sessions attach-leader collect diagnose repair-state validate-result install-skill e2e"
417
+ ),
418
+ formatter_class=argparse.RawDescriptionHelpFormatter,
419
+ )
420
+ p.set_defaults(func=cmd_advanced)
421
+
422
+ sub._choices_actions = [ # type: ignore[attr-defined]
423
+ action for action in sub._choices_actions if action.help != argparse.SUPPRESS # type: ignore[attr-defined]
424
+ ]
425
+ sub.metavar = "{codex,claude,quick-start,send,status,approvals,inbox,shutdown,restart,start-agent,stop-agent,reset-agent,add-agent,fork-agent,remove-agent,stuck-list,stuck-cancel,doctor}"
426
+
427
+ args = parser.parse_args(raw_argv)
428
+ try:
429
+ result = args.func(args)
430
+ except TeamAgentError as exc:
431
+ _emit_cli_error(exc, args)
432
+ raise SystemExit(1)
433
+ except Exception as exc:
434
+ _emit_cli_error(exc, args)
435
+ raise SystemExit(1)
436
+ emit(result, getattr(args, "json", False))
437
+ if isinstance(result, dict) and result.get("ok") is False:
438
+ raise SystemExit(1)
439
+
440
+
441
+ def add_json(parser: argparse.ArgumentParser) -> None:
442
+ parser.add_argument("--json", action="store_true", help="Emit stable machine-readable JSON")
443
+
444
+
445
+ def _run_leader_passthrough(command: str, provider_args: list[str]) -> None:
446
+ if provider_args in (["-h"], ["--help"]):
447
+ print(f"usage: team-agent {command} [--attach --confirm | --attach-session SESSION --confirm] [args passed to {command}]")
448
+ print()
449
+ print(f"Start a tmux-managed {command} leader in the current directory.")
450
+ print("Default starts a new independent leader session; explicit attach requires --confirm.")
451
+ print(f"Use `team-agent {command} -- --help` to pass --help to the provider CLI.")
452
+ return
453
+ args = argparse.Namespace(command=command, workspace=".")
454
+ try:
455
+ provider = "codex" if command == "codex" else "claude_code"
456
+ launcher_args = _leader_launcher_args(provider_args)
457
+ runtime.start_leader(
458
+ provider,
459
+ _provider_args(launcher_args["provider_args"]),
460
+ Path.cwd().resolve(),
461
+ attach_existing=launcher_args["attach_existing"],
462
+ confirm_attach=launcher_args["confirm_attach"],
463
+ attach_session=launcher_args["attach_session"],
464
+ )
465
+ except TeamAgentError as exc:
466
+ _emit_cli_error(exc, args)
467
+ raise SystemExit(1)
468
+ except Exception as exc:
469
+ _emit_cli_error(exc, args)
470
+ raise SystemExit(1)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
3
4
  from pathlib import Path
4
5
  from typing import Any
5
6
 
@@ -39,40 +40,19 @@ def compile_team(team_dir: Path, out_path: Path | None = None) -> dict[str, Any]
39
40
  routing_rules = []
40
41
  startup_order = []
41
42
  for role_doc in sorted(agents_dir.glob("*.md")):
42
- meta, body = _read_front_matter(role_doc)
43
- if "auth_mode" not in meta and default_auth_mode is not None:
44
- meta["auth_mode"] = default_auth_mode
45
- if "profile" not in meta and default_profile is not None:
46
- meta["profile"] = default_profile
47
- profile_model = _profile_model(workspace, meta.get("profile"), team_dir / "profiles")
48
- if (
49
- "model" not in meta
50
- and not (meta.get("auth_mode") == "compatible_api" and profile_model)
51
- ):
52
- meta["model"] = _default_model_for_provider(meta.get("provider"), provider_models, default_model)
53
- _validate_role_doc(role_doc, meta, body, profile_names, profile_model)
43
+ meta, body = _role_doc_meta_for_team(
44
+ role_doc,
45
+ team_meta,
46
+ workspace,
47
+ team_dir,
48
+ profile_names,
49
+ default_auth_mode,
50
+ default_profile,
51
+ provider_models,
52
+ default_model,
53
+ )
54
54
  agent_id = str(meta["name"])
55
- tools = _normalize_tools(list(meta["tools"] or []))
56
- agent = {
57
- "id": agent_id,
58
- "role": str(meta["role"]),
59
- "provider": str(meta["provider"]),
60
- "model": str(meta["model"]) if meta.get("model") is not None else None,
61
- "auth_mode": str(meta["auth_mode"]),
62
- "working_directory": str(workspace),
63
- "system_prompt": {"inline": body.strip() or str(meta["role"]), "file": None},
64
- "tools": tools,
65
- "permission_mode": "restricted",
66
- "preferred_for": [agent_id, str(meta["role"])],
67
- "avoid_for": [],
68
- "output_contract": {
69
- "format": "result_envelope_v1",
70
- "required_fields": ["task_id", "status", "summary", "artifacts"],
71
- },
72
- }
73
- if meta.get("profile"):
74
- agent["profile"] = str(meta["profile"])
75
- agent["credential_ref"] = f"profile:{meta['profile']}"
55
+ agent = _agent_from_role_doc(meta, body, workspace, agent_id)
76
56
  agents.append(agent)
77
57
  routing_rules.append({"id": f"route-{agent_id}", "match": {"assignee": [agent_id]}, "assign_to": agent_id, "priority": 10})
78
58
  startup_order.append(agent_id)
@@ -155,6 +135,41 @@ def compile_team(team_dir: Path, out_path: Path | None = None) -> dict[str, Any]
155
135
  return {"ok": True, "team_dir": str(team_dir), "out": str(out_path) if out_path else None, "spec": spec}
156
136
 
157
137
 
138
+ def compile_role_doc_agent(role_doc: Path, team_dir: Path, agent_id: str | None = None) -> dict[str, Any]:
139
+ meta, body = _read_front_matter(role_doc.resolve())
140
+ return compile_role_entry_agent(role_doc.resolve(), team_dir, meta, body, agent_id)
141
+
142
+
143
+ def compile_role_entry_agent(
144
+ role_doc: Path,
145
+ team_dir: Path,
146
+ meta: dict[str, Any],
147
+ body: str,
148
+ agent_id: str | None = None,
149
+ ) -> dict[str, Any]:
150
+ team_dir = team_dir.resolve()
151
+ workspace = team_workspace(team_dir)
152
+ team_doc = team_dir / "TEAM.md"
153
+ if not team_doc.exists():
154
+ raise ValidationError(f"{team_doc}: missing TEAM.md")
155
+ profile_names = known_profiles(team_dir)
156
+ team_meta, _team_body = _read_front_matter(team_doc)
157
+ meta, body = _role_doc_meta_for_team(
158
+ role_doc,
159
+ team_meta,
160
+ workspace,
161
+ team_dir,
162
+ profile_names,
163
+ team_meta.get("default_auth_mode") or "subscription",
164
+ team_meta.get("default_profile"),
165
+ _provider_model_defaults(team_meta),
166
+ team_meta.get("default_model") or team_meta.get("model"),
167
+ role_meta=meta,
168
+ role_body=body,
169
+ )
170
+ return _agent_from_role_doc(meta, body, workspace, str(agent_id or meta["name"]))
171
+
172
+
158
173
  def _read_front_matter(path: Path) -> tuple[dict[str, Any], str]:
159
174
  text = path.read_text(encoding="utf-8")
160
175
  if not text.startswith("---\n"):
@@ -170,6 +185,56 @@ def _read_front_matter(path: Path) -> tuple[dict[str, Any], str]:
170
185
  return data, body
171
186
 
172
187
 
188
+ def _role_doc_meta_for_team(
189
+ role_doc: Path,
190
+ team_meta: dict[str, Any],
191
+ workspace: Path,
192
+ team_dir: Path,
193
+ profile_names: set[str],
194
+ default_auth_mode: Any,
195
+ default_profile: Any,
196
+ provider_models: dict[str, str],
197
+ default_model: Any,
198
+ role_meta: dict[str, Any] | None = None,
199
+ role_body: str | None = None,
200
+ ) -> tuple[dict[str, Any], str]:
201
+ meta, body = (role_meta, role_body) if role_meta is not None and role_body is not None else _read_front_matter(role_doc)
202
+ meta = copy.deepcopy(meta)
203
+ if "auth_mode" not in meta and default_auth_mode is not None:
204
+ meta["auth_mode"] = default_auth_mode
205
+ if "profile" not in meta and default_profile is not None:
206
+ meta["profile"] = default_profile
207
+ profile_model = _profile_model(workspace, meta.get("profile"), team_dir / "profiles")
208
+ if "model" not in meta and not (meta.get("auth_mode") == "compatible_api" and profile_model):
209
+ meta["model"] = _default_model_for_provider(meta.get("provider"), provider_models, default_model)
210
+ _validate_role_doc(role_doc, meta, body, profile_names, profile_model)
211
+ return meta, body
212
+
213
+
214
+ def _agent_from_role_doc(meta: dict[str, Any], body: str, workspace: Path, agent_id: str) -> dict[str, Any]:
215
+ agent = {
216
+ "id": agent_id,
217
+ "role": str(meta["role"]),
218
+ "provider": str(meta["provider"]),
219
+ "model": str(meta["model"]) if meta.get("model") is not None else None,
220
+ "auth_mode": str(meta["auth_mode"]),
221
+ "working_directory": str(workspace),
222
+ "system_prompt": {"inline": body.strip() or str(meta["role"]), "file": None},
223
+ "tools": _normalize_tools(list(meta["tools"] or [])),
224
+ "permission_mode": "restricted",
225
+ "preferred_for": [agent_id, str(meta["role"])],
226
+ "avoid_for": [],
227
+ "output_contract": {
228
+ "format": "result_envelope_v1",
229
+ "required_fields": ["task_id", "status", "summary", "artifacts"],
230
+ },
231
+ }
232
+ if meta.get("profile"):
233
+ agent["profile"] = str(meta["profile"])
234
+ agent["credential_ref"] = f"profile:{meta['profile']}"
235
+ return agent
236
+
237
+
173
238
  def _validate_role_doc(
174
239
  path: Path,
175
240
  meta: dict[str, Any],
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from team_agent.coordinator.lifecycle import (
6
+ coordinator_health,
7
+ coordinator_tick,
8
+ message_store_schema_health,
9
+ start_coordinator,
10
+ stop_coordinator,
11
+ )
12
+ from team_agent.coordinator.metadata import (
13
+ COORDINATOR_PROTOCOL_VERSION,
14
+ coordinator_metadata_ok,
15
+ pid_is_running,
16
+ read_coordinator_metadata,
17
+ write_coordinator_metadata,
18
+ )
19
+ from team_agent.coordinator.paths import (
20
+ coordinator_log_path,
21
+ coordinator_meta_path,
22
+ coordinator_pid_path,
23
+ )
24
+
25
+ __all__ = [
26
+ "COORDINATOR_PROTOCOL_VERSION",
27
+ "coordinator_health",
28
+ "coordinator_log_path",
29
+ "coordinator_meta_path",
30
+ "coordinator_metadata_ok",
31
+ "coordinator_pid_path",
32
+ "coordinator_tick",
33
+ "main",
34
+ "message_store_schema_health",
35
+ "pid_is_running",
36
+ "read_coordinator_metadata",
37
+ "start_coordinator",
38
+ "stop_coordinator",
39
+ "write_coordinator_metadata",
40
+ ]
41
+
42
+
43
+ def __getattr__(name: str) -> Any:
44
+ # Lazy re-export of the daemon entry so the pyproject console_script
45
+ # `team-agent-coordinator = "team_agent.coordinator:main"` keeps
46
+ # resolving after the package split, without triggering the runtime
47
+ # <-> coordinator import cycle that an eager top-level
48
+ # `from team_agent.coordinator.__main__ import main` would cause
49
+ # (runtime imports coordinator/__init__ at module load).
50
+ if name == "main":
51
+ from team_agent.coordinator.__main__ import main as _main
52
+ return _main
53
+ raise AttributeError(f"module 'team_agent.coordinator' has no attribute {name!r}")