multi-agent-platform 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. cli/__init__.py +0 -0
  2. cli/action_item_escalation.py +177 -0
  3. cli/agent_client.py +554 -0
  4. cli/bridge_state.py +43 -0
  5. cli/commands/__init__.py +13 -0
  6. cli/commands/action.py +142 -0
  7. cli/commands/agent.py +117 -0
  8. cli/commands/audit.py +68 -0
  9. cli/commands/docs.py +179 -0
  10. cli/commands/experiment.py +755 -0
  11. cli/commands/feedback.py +106 -0
  12. cli/commands/notification.py +213 -0
  13. cli/commands/persona.py +63 -0
  14. cli/commands/project.py +87 -0
  15. cli/commands/runtime.py +105 -0
  16. cli/commands/topic.py +361 -0
  17. cli/e2e_collab.py +602 -0
  18. cli/git_checkpoint.py +68 -0
  19. cli/host_worker_types.py +151 -0
  20. cli/main.py +1553 -0
  21. cli/map_command_client.py +497 -0
  22. cli/participant_worker.py +255 -0
  23. cli/reviewer_worker.py +263 -0
  24. cli/runtime/__init__.py +5 -0
  25. cli/runtime/run_lock.py +497 -0
  26. cli/runtime_chat.py +317 -0
  27. cli/session_wake_log.py +235 -0
  28. cli/simple_waker.py +950 -0
  29. cli/table_render.py +113 -0
  30. cli/wake_backend.py +236 -0
  31. cli/worker_cycle_log.py +36 -0
  32. map_client/__init__.py +37 -0
  33. map_client/bootstrap.py +193 -0
  34. map_client/client.py +1045 -0
  35. map_client/config.py +21 -0
  36. map_client/errors.py +283 -0
  37. map_client/exceptions.py +130 -0
  38. map_client/plan_evidence.py +159 -0
  39. map_client/project_config.py +153 -0
  40. map_client/result_template.py +167 -0
  41. map_client/testing.py +27 -0
  42. map_mcp/__init__.py +4 -0
  43. map_mcp/_utils.py +28 -0
  44. map_mcp/auth.py +34 -0
  45. map_mcp/config.py +50 -0
  46. map_mcp/context.py +39 -0
  47. map_mcp/main.py +75 -0
  48. map_mcp/server.py +573 -0
  49. map_mcp/session.py +79 -0
  50. map_sdk/__init__.py +29 -0
  51. map_sdk/evidence.py +68 -0
  52. map_types/__init__.py +203 -0
  53. map_types/enums.py +199 -0
  54. map_types/schemas.py +1351 -0
  55. multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
  56. multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
  57. multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
  58. multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
  59. multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
  61. server/__init__.py +0 -0
  62. server/__version__.py +14 -0
  63. server/api/__init__.py +0 -0
  64. server/api/action_items.py +138 -0
  65. server/api/agents.py +412 -0
  66. server/api/audit.py +54 -0
  67. server/api/background_tasks.py +18 -0
  68. server/api/common.py +117 -0
  69. server/api/deps.py +30 -0
  70. server/api/experiments.py +858 -0
  71. server/api/feedback.py +75 -0
  72. server/api/notifications.py +22 -0
  73. server/api/projects.py +209 -0
  74. server/api/router.py +25 -0
  75. server/api/status.py +33 -0
  76. server/api/topics.py +302 -0
  77. server/api/webhooks.py +74 -0
  78. server/auth/__init__.py +8 -0
  79. server/auth/experiment_access.py +66 -0
  80. server/config.py +38 -0
  81. server/db/__init__.py +3 -0
  82. server/db/base.py +5 -0
  83. server/db/deadlock_retry.py +146 -0
  84. server/db/session.py +41 -0
  85. server/domain/__init__.py +3 -0
  86. server/domain/encrypted_types.py +63 -0
  87. server/domain/models.py +713 -0
  88. server/domain/schemas.py +3 -0
  89. server/domain/state_machine.py +79 -0
  90. server/domain/topic_ack_constants.py +9 -0
  91. server/main.py +148 -0
  92. server/scripts/__init__.py +0 -0
  93. server/scripts/migrate_notification_unique.py +231 -0
  94. server/scripts/purge_audit_pollution.py +116 -0
  95. server/services/__init__.py +0 -0
  96. server/services/_lookups.py +26 -0
  97. server/services/acceptance_service.py +90 -0
  98. server/services/action_item_migration_service.py +190 -0
  99. server/services/action_item_service.py +200 -0
  100. server/services/agent_work_service.py +405 -0
  101. server/services/archive_lint_service.py +156 -0
  102. server/services/audit_service.py +457 -0
  103. server/services/auth.py +66 -0
  104. server/services/comment_service.py +173 -0
  105. server/services/errors.py +65 -0
  106. server/services/escalation_resolver.py +248 -0
  107. server/services/evidence_service.py +88 -0
  108. server/services/experiment_capabilities_service.py +277 -0
  109. server/services/inbound_event_service.py +111 -0
  110. server/services/lock_service.py +273 -0
  111. server/services/log_service.py +202 -0
  112. server/services/mention_service.py +730 -0
  113. server/services/notification_service.py +939 -0
  114. server/services/notification_stream.py +138 -0
  115. server/services/permissions.py +147 -0
  116. server/services/persona_activity_service.py +108 -0
  117. server/services/phase_owner_resolver.py +95 -0
  118. server/services/phase_service.py +381 -0
  119. server/services/plan_marker_service.py +235 -0
  120. server/services/plan_service.py +186 -0
  121. server/services/platform_feedback_service.py +114 -0
  122. server/services/project_service.py +534 -0
  123. server/services/project_status_service.py +132 -0
  124. server/services/review_service.py +707 -0
  125. server/services/secret_encryption.py +97 -0
  126. server/services/similarity_service.py +119 -0
  127. server/services/sse_event_schemas.py +17 -0
  128. server/services/status_service.py +68 -0
  129. server/services/template_service.py +134 -0
  130. server/services/text_utils.py +19 -0
  131. server/services/thread_activity.py +180 -0
  132. server/services/todo_persona_filter.py +73 -0
  133. server/services/todo_service.py +604 -0
  134. server/services/topic_ack_service.py +312 -0
  135. server/services/topic_action_item_ops.py +538 -0
  136. server/services/topic_comment_kind.py +14 -0
  137. server/services/topic_comment_service.py +237 -0
  138. server/services/topic_helpers.py +32 -0
  139. server/services/topic_lifecycle_service.py +478 -0
  140. server/services/topic_progress_service.py +40 -0
  141. server/services/topic_resolve_service.py +234 -0
  142. server/services/topic_service.py +102 -0
  143. server/services/topic_work_item_service.py +570 -0
  144. server/services/webhook_service.py +273 -0
@@ -0,0 +1,106 @@
1
+ """``map feedback ...`` sub-app — arch experiment 0519e2a3 PR7.
2
+
3
+ Platform feedback inbox. ``submit`` is persona-scoped; ``list`` / ``get`` /
4
+ ``update`` are admin-only (used for triage).
5
+
6
+ All command bodies lazy-import ``cli.main._run`` to break the
7
+ ``cli.main ↔ cli.commands.*`` import cycle.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import uuid
12
+ from pathlib import Path
13
+
14
+ import typer
15
+ from map_client.client import MAPClient
16
+
17
+ feedback_app = typer.Typer(help="Platform feedback inbox commands")
18
+
19
+
20
+ @feedback_app.command("submit")
21
+ def feedback_submit(
22
+ body: str | None = typer.Option(None, "--body", help="Feedback text (free-form)."),
23
+ body_file: Path | None = typer.Option(
24
+ None,
25
+ "--file",
26
+ help="Read feedback body from a file (avoids shell-quoting issues with backticks / $vars).",
27
+ ),
28
+ category: str | None = typer.Option(
29
+ None, "--category", help="bug|suggestion|question|other (optional, admin triage hint)"
30
+ ),
31
+ project: uuid.UUID | None = typer.Option(
32
+ None, "--project", help="Source project context (optional)"
33
+ ),
34
+ ) -> None:
35
+ from map_types.enums import FeedbackCategory
36
+
37
+ from cli.main import _read_text_file, _run # lazy: avoid cli.main ↔ cli.commands.* cycle
38
+ from server.domain.schemas import PlatformFeedbackCreate
39
+
40
+ if body is None and body_file is None:
41
+ typer.echo("Error: either --body or --file is required", err=True)
42
+ raise typer.Exit(2)
43
+ if body is not None and body_file is not None:
44
+ typer.echo("Error: use only one of --body or --file", err=True)
45
+ raise typer.Exit(2)
46
+ content = body if body is not None else _read_text_file(body_file, kind="feedback")
47
+ payload = PlatformFeedbackCreate(
48
+ body=content,
49
+ project_id=project,
50
+ category=FeedbackCategory(category) if category else None,
51
+ )
52
+ _run(lambda c: c.submit_feedback(payload))
53
+
54
+
55
+ @feedback_app.command("list")
56
+ def feedback_list(
57
+ status: str | None = typer.Option(None, "--status"),
58
+ category: str | None = typer.Option(None, "--category"),
59
+ project: uuid.UUID | None = typer.Option(None, "--project"),
60
+ page: int = typer.Option(1, "--page", min=1),
61
+ page_size: int = typer.Option(50, "--page-size", min=1, max=200),
62
+ include_archived: bool = typer.Option(False, "--include-archived"),
63
+ ) -> None:
64
+ from map_types.enums import FeedbackCategory, FeedbackStatus
65
+
66
+ from cli.main import _run # lazy
67
+
68
+ def action(c: MAPClient):
69
+ items, total = c.list_feedback_page(
70
+ status=FeedbackStatus(status) if status else None,
71
+ category=FeedbackCategory(category) if category else None,
72
+ project_id=project,
73
+ page=page,
74
+ page_size=page_size,
75
+ include_archived=include_archived,
76
+ )
77
+ return {"items": items, "total": total}
78
+
79
+ _run(action, admin=True)
80
+
81
+
82
+ @feedback_app.command("get")
83
+ def feedback_get(feedback_id: uuid.UUID = typer.Argument(..., help="Feedback UUID")) -> None:
84
+ from cli.main import _run # lazy
85
+
86
+ _run(lambda c: c.get_feedback(feedback_id), admin=True)
87
+
88
+
89
+ @feedback_app.command("update")
90
+ def feedback_update(
91
+ feedback_id: uuid.UUID = typer.Argument(..., help="Feedback UUID"),
92
+ status: str | None = typer.Option(None, "--status"),
93
+ category: str | None = typer.Option(None, "--category"),
94
+ archived: bool | None = typer.Option(None, "--archived/--no-archived"),
95
+ ) -> None:
96
+ from map_types.enums import FeedbackCategory, FeedbackStatus
97
+
98
+ from cli.main import _run # lazy
99
+ from server.domain.schemas import PlatformFeedbackUpdate
100
+
101
+ payload = PlatformFeedbackUpdate(
102
+ status=FeedbackStatus(status) if status else None,
103
+ category=FeedbackCategory(category) if category else None,
104
+ archived=archived,
105
+ )
106
+ _run(lambda c: c.update_feedback(feedback_id, payload), admin=True)
@@ -0,0 +1,213 @@
1
+ """``map notification ...`` + ``map inbound-event ...`` sub-apps — arch PR5.
2
+
3
+ Wraps the personal-inbox / waker-event surface:
4
+
5
+ * ``map notification list`` —
6
+ ``GET /api/v1/notifications`` with category/target_type/unread_only filters.
7
+ * ``map notification read`` —
8
+ ``POST /notifications/{id}/read`` (one notification at a time).
9
+ * ``map notification read-all`` (cli-ux PR2: + ``--category`` + ``--event``) —
10
+ ``POST /notifications/read-all`` for the unfiltered fast path, OR enumerate
11
+ via ``list`` + ``mark_notification_read`` for filtered bulk. Mirrors the
12
+ wakeable / digest view surfaced in the web todo UI.
13
+ * ``map inbound-event record`` —
14
+ ``POST /api/v1/agents/me/inbound-events``. Waker-side D6 gate;
15
+ on 409 the CLI exits non-zero so the waker treats it as
16
+ "already woken" and skips resume.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import uuid
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import typer
26
+ from map_client.client import MAPClient
27
+ from map_client.exceptions import MAPConflictError
28
+
29
+ from cli.table_render import enum_value, format_datetime, render_table, short_uuid, truncate
30
+
31
+ notification_app = typer.Typer(help="Notification commands (personal inbox)")
32
+
33
+
34
+ def _render_notification_table(result: Any) -> str:
35
+ """Render a NotificationListRead as a compact table with summary footer."""
36
+ items = result.items if hasattr(result, "items") else result
37
+ total = getattr(result, "total", len(items))
38
+ unread = getattr(result, "unread_count", 0)
39
+
40
+ headers = ["ID", "Event", "Summary", "Category", "Read", "Created"]
41
+ rows = []
42
+ for n in items:
43
+ rows.append([
44
+ short_uuid(n.id),
45
+ truncate(n.event, 30),
46
+ truncate(n.summary, 50),
47
+ enum_value(n.category),
48
+ "yes" if n.read_at else "no",
49
+ format_datetime(n.created_at),
50
+ ])
51
+ table = render_table(headers, rows)
52
+ footer = f"\n({len(items)} shown, {unread} unread / {total} total)"
53
+ return table + footer
54
+
55
+
56
+ @notification_app.command("list")
57
+ def notification_list(
58
+ unread_only: bool = typer.Option(False, "--unread-only", help="Only show unread."),
59
+ category: str | None = typer.Option(
60
+ None, "--category", help="wakeable | digest | all (filter by category)."
61
+ ),
62
+ target_type: str | None = typer.Option(
63
+ None, "--target-type", help="Filter by notification.target_type."
64
+ ),
65
+ limit: int = typer.Option(50, "--limit", min=1, max=200),
66
+ offset: int = typer.Option(0, "--offset", min=0),
67
+ ) -> None:
68
+ """List the current persona's notifications.
69
+
70
+ Defaults to a compact table view. Use ``--format yaml`` or
71
+ ``--format json`` for full structured output (scripts / piping).
72
+ """
73
+ from cli.main import _run # lazy: avoid cli.main ↔ cli.commands.* cycle
74
+
75
+ def action(c: Any) -> Any:
76
+ return c.list_notifications(
77
+ unread_only=unread_only,
78
+ category=category,
79
+ target_type=target_type,
80
+ limit=limit,
81
+ offset=offset,
82
+ )
83
+
84
+ _run(action, table_renderer=_render_notification_table)
85
+
86
+
87
+ @notification_app.command("read")
88
+ def notification_read(
89
+ notification_id: uuid.UUID = typer.Option(..., "--id", help="Notification UUID."),
90
+ ) -> None:
91
+ """Mark one notification as read."""
92
+ from cli.main import _run
93
+
94
+ def action(c: Any) -> Any:
95
+ return c.mark_notification_read(notification_id)
96
+
97
+ _run(action)
98
+
99
+
100
+ @notification_app.command("read-all")
101
+ def notification_read_all(
102
+ category: str | None = typer.Option(
103
+ None,
104
+ "--category",
105
+ help="Filter by category before bulk-marking: wakeable | digest | all (default = all).",
106
+ ),
107
+ event: str | None = typer.Option(
108
+ None,
109
+ "--event",
110
+ help="Filter by notification.event before bulk-marking "
111
+ "(client-side filter; e.g. mention.created, experiment.phase_changed).",
112
+ ),
113
+ ) -> None:
114
+ """Mark every (filtered) notification for the current persona as read.
115
+
116
+ Without filters this hits the bulk ``POST /agents/me/notifications/read-all``
117
+ endpoint in a single round-trip. With ``--category`` or ``--event`` set, the
118
+ CLI enumerates the matching subset via ``list_notifications`` then marks each
119
+ via ``mark_notification_read`` (still fewer round-trips than ``read --id``
120
+ one-at-a-time, and doesn't require knowing IDs upfront).
121
+ """
122
+ from map_types.enums import NotificationCategory
123
+
124
+ from cli.main import _run
125
+
126
+ # Validate --category early so user sees a clean error before any API call.
127
+ if category is not None and category not in ("wakeable", "digest", "all"):
128
+ typer.echo(
129
+ f"Error: --category must be one of wakeable|digest|all (got {category!r})",
130
+ err=True,
131
+ )
132
+ raise typer.Exit(2)
133
+
134
+ def action(c: Any) -> Any:
135
+ if category is None and event is None:
136
+ # Fast path: single bulk endpoint, no enumeration.
137
+ return c.mark_all_notifications_read()
138
+
139
+ # Filtered path: enumerate, then mark each match.
140
+ list_kwargs: dict = {"unread_only": True, "limit": 200}
141
+ if category is not None and category != "all":
142
+ list_kwargs["category"] = NotificationCategory(category)
143
+ # Pagination loop — most personas have < 200 unread, but loop if more.
144
+ marked = 0
145
+ offset = 0
146
+ while True:
147
+ list_kwargs["offset"] = offset
148
+ page = c.list_notifications(**list_kwargs)
149
+ if not page.items:
150
+ break
151
+ for n in page.items:
152
+ if event is not None and getattr(n, "event", None) != event:
153
+ continue
154
+ c.mark_notification_read(n.id)
155
+ marked += 1
156
+ offset += len(page.items)
157
+ if offset >= page.total:
158
+ break
159
+ return {"marked": marked}
160
+
161
+ _run(action)
162
+
163
+
164
+ inbound_event_app = typer.Typer(
165
+ help="Runtime-waker inbound event commands (D6 server gate)"
166
+ )
167
+
168
+
169
+ @inbound_event_app.command("record")
170
+ def inbound_event_record(
171
+ event_id: uuid.UUID = typer.Option(
172
+ ..., "--event-id", help="Upstream notification id (UUID)."
173
+ ),
174
+ fingerprint: str = typer.Option(
175
+ ..., "--fingerprint", help="Dedup key (server enforces UNIQUE per agent)."
176
+ ),
177
+ event_type: str = typer.Option(
178
+ ...,
179
+ "--event-type",
180
+ help="Logical event type (e.g. mention, pending_review, topic_lifecycle).",
181
+ ),
182
+ source: str = typer.Option(
183
+ "polling", "--source", help="polling | sse | replay (Phase 1 = polling)."
184
+ ),
185
+ payload_file: str | None = typer.Option(
186
+ None, "--payload-file", help="Optional JSON file with extra payload fields."
187
+ ),
188
+ ) -> None:
189
+ """Record that the caller is about to act on ``event_id``."""
190
+ from map_types.enums import InboundEventSource
191
+
192
+ from cli.main import _read_text_file, _run
193
+ from server.domain.schemas import InboundEventCreate
194
+
195
+ extra_payload: dict | None = None
196
+ if payload_file is not None:
197
+ extra_payload = json.loads(_read_text_file(Path(payload_file), kind="payload"))
198
+ payload = InboundEventCreate(
199
+ event_id=event_id,
200
+ event_type=event_type,
201
+ source=InboundEventSource(source),
202
+ fingerprint=fingerprint,
203
+ payload=extra_payload,
204
+ )
205
+
206
+ def action(c: MAPClient):
207
+ try:
208
+ return c.record_inbound_event(payload)
209
+ except MAPConflictError as exc:
210
+ typer.echo(f"inbound-event duplicate (409): {exc.detail}", err=True)
211
+ raise typer.Exit(2) from exc
212
+
213
+ _run(action)
@@ -0,0 +1,63 @@
1
+ """``map persona ...`` sub-app — cli/main.py split.
2
+
3
+ Local ``.map/agents.yaml`` identity commands.
4
+ Command bodies lazy-import ``cli.main`` helpers to break the import cycle.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ from map_client.client import MAPClient
12
+ from map_client.project_config import find_map_dir, load_project_map_config
13
+
14
+ persona_app = typer.Typer(help="Persona / identity commands")
15
+
16
+
17
+ @persona_app.command("list")
18
+ def persona_list(
19
+ project_root: Path | None = typer.Option(None, "--project-root"),
20
+ ) -> None:
21
+ """List personas defined in .map/agents.yaml."""
22
+ from cli.main import _print_json, _require_map_dir # lazy: avoid cycle
23
+ map_dir = _require_map_dir(project_root)
24
+ cfg = load_project_map_config(map_dir=map_dir)
25
+ rows = []
26
+ for key, info in cfg.personas.items():
27
+ has_token = key in cfg.tokens
28
+ rows.append(
29
+ {
30
+ "persona": key,
31
+ "agent_name": info.agent_name,
32
+ "has_token": has_token,
33
+ "description": info.description,
34
+ }
35
+ )
36
+ _print_json(rows)
37
+
38
+
39
+ @persona_app.command("whoami")
40
+ def persona_whoami(
41
+ persona: str | None = typer.Option(None, "--persona", "-p"),
42
+ project_root: Path | None = typer.Option(None, "--project-root"),
43
+ ) -> None:
44
+ """Show MAP identity for the selected persona (default from .map/config.yaml)."""
45
+ from cli.main import _cli_options, _run # lazy: avoid cycle
46
+ if persona is not None:
47
+ _cli_options["persona"] = persona
48
+ if project_root is not None:
49
+ _cli_options["project_root"] = project_root
50
+
51
+ def action(c: MAPClient):
52
+ me = c.get_me()
53
+ payload = me.model_dump(mode="json")
54
+ if _cli_options.get("persona"):
55
+ payload["persona"] = _cli_options["persona"]
56
+ elif find_map_dir(_cli_options.get("project_root")):
57
+ payload["persona"] = load_project_map_config(
58
+ project_root=_cli_options.get("project_root")
59
+ ).default_persona
60
+ return payload
61
+
62
+ _run(action)
63
+
@@ -0,0 +1,87 @@
1
+ """``map project ...`` sub-app — cli/main.py split.
2
+
3
+ Command bodies lazy-import ``cli.main`` helpers to break the import cycle.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ from map_client.client import MAPClient
12
+
13
+ from server.domain.schemas import ProjectStatusRevise
14
+
15
+ project_app = typer.Typer(help="Project commands")
16
+ status_app = typer.Typer(help="Project Current Status commands")
17
+ project_app.add_typer(status_app, name="status")
18
+
19
+
20
+ @project_app.command("create")
21
+ def project_create(
22
+ key: str = typer.Option(..., "--key"),
23
+ name: str = typer.Option(..., "--name"),
24
+ path: str = typer.Option(..., "--path"),
25
+ description: str | None = typer.Option(None, "--description"),
26
+ ) -> None:
27
+ from cli.main import _run # lazy: avoid cycle
28
+ _run(lambda c: c.create_project(key, name, path, description))
29
+
30
+
31
+ @project_app.command("list")
32
+ def project_list(include_archived: bool = typer.Option(False, "--include-archived")) -> None:
33
+ from cli.main import _run # lazy: avoid cycle
34
+ _run(lambda c: c.list_projects(include_archived=include_archived))
35
+
36
+
37
+ @project_app.command("decisions")
38
+ def project_decisions(
39
+ project: uuid.UUID | None = typer.Option(None, "--project"),
40
+ project_key: str | None = typer.Option(None, "--project-key"),
41
+ limit: int = typer.Option(20, "--limit", min=1, max=100),
42
+ ) -> None:
43
+ from cli.main import _resolve_project, _run # lazy: avoid cycle
44
+ def action(c: MAPClient):
45
+ pid = _resolve_project(c, project, project_key)
46
+ return c.list_project_decisions(pid, limit=limit)
47
+
48
+ _run(action)
49
+
50
+
51
+ @status_app.command("revise")
52
+ def project_status_revise(
53
+ status_file: Path = typer.Option(..., "--file"),
54
+ project: uuid.UUID | None = typer.Option(None, "--project"),
55
+ project_key: str | None = typer.Option(None, "--project-key"),
56
+ note: str | None = typer.Option(None, "--note"),
57
+ ) -> None:
58
+ from cli.main import _read_text_file, _resolve_project, _run # lazy: avoid cycle
59
+ payload = ProjectStatusRevise(content_md=_read_text_file(status_file, kind="status"), change_note=note)
60
+
61
+ def action(c: MAPClient):
62
+ pid = _resolve_project(c, project, project_key)
63
+ return c.revise_project_status(pid, payload)
64
+
65
+ _run(action)
66
+
67
+
68
+ @status_app.command("versions")
69
+ def project_status_versions(
70
+ project: uuid.UUID | None = typer.Option(None, "--project"),
71
+ project_key: str | None = typer.Option(None, "--project-key"),
72
+ ) -> None:
73
+ from cli.main import _resolve_project, _run # lazy: avoid cycle
74
+ _run(lambda c: c.list_project_status_versions(_resolve_project(c, project, project_key)))
75
+
76
+
77
+ @status_app.command("show")
78
+ def project_status_show(
79
+ version: int = typer.Option(..., "--version"),
80
+ project: uuid.UUID | None = typer.Option(None, "--project"),
81
+ project_key: str | None = typer.Option(None, "--project-key"),
82
+ ) -> None:
83
+ from cli.main import _resolve_project, _run # lazy: avoid cycle
84
+ _run(
85
+ lambda c: c.get_project_status_version(_resolve_project(c, project, project_key), version)
86
+ )
87
+
@@ -0,0 +1,105 @@
1
+ """``map runtime ...`` sub-app — cli/main.py split.
2
+
3
+ Command bodies lazy-import ``cli.main`` helpers to break the import cycle.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ runtime_app = typer.Typer(help="Agent runtime session commands")
12
+
13
+
14
+ @runtime_app.command("chat")
15
+ def runtime_chat(
16
+ persona: str = typer.Option(
17
+ "host",
18
+ "--persona",
19
+ "-p",
20
+ help="Persona whose runtime session to resume (default: host)",
21
+ ),
22
+ project_root: Path | None = typer.Option(
23
+ None,
24
+ "--project-root",
25
+ help="Repo root containing .map/ (default: search upward from cwd)",
26
+ ),
27
+ state_file: Path | None = typer.Option(
28
+ None,
29
+ "--state-file",
30
+ help="Runtime waker state file (default: .map/runtime-waker-state-<persona>.json)",
31
+ ),
32
+ runtime_home: Path | None = typer.Option(
33
+ None,
34
+ "--runtime-home",
35
+ help="Claude runtime HOME (default: .map/claude-runtime-home-<persona>)",
36
+ ),
37
+ session_id: str | None = typer.Option(
38
+ None,
39
+ "--session-id",
40
+ help="Resume a specific Claude session id (default: read from state file)",
41
+ ),
42
+ prompt: str | None = typer.Option(
43
+ None,
44
+ "--prompt",
45
+ help="Send one prompt before entering interactive REPL",
46
+ ),
47
+ new_session: bool = typer.Option(
48
+ False,
49
+ "--new-session",
50
+ help="Start a fresh Claude session instead of resuming state",
51
+ ),
52
+ ignore_waker: bool = typer.Option(
53
+ False,
54
+ "--ignore-waker",
55
+ help="Allow chat while map-runtime-waker is running (may conflict)",
56
+ ),
57
+ model: str | None = typer.Option(None, "--model", help="Optional Claude model override"),
58
+ ) -> None:
59
+ """Resume a persona runtime session and chat interactively from the terminal."""
60
+ from cli.main import _cli_options # lazy: avoid cycle
61
+ from cli.runtime_chat import run_runtime_chat
62
+
63
+ run_runtime_chat(
64
+ persona=persona,
65
+ project_root=project_root or _cli_options.get("project_root"),
66
+ state_file=state_file,
67
+ runtime_home=runtime_home,
68
+ session_id=session_id,
69
+ new_session=new_session,
70
+ initial_prompt=prompt,
71
+ ignore_waker=ignore_waker,
72
+ model=model,
73
+ )
74
+
75
+
76
+ @runtime_app.command("status")
77
+ def runtime_status(
78
+ persona: str = typer.Option(
79
+ "host",
80
+ "--persona",
81
+ "-p",
82
+ help="Persona to inspect (default: host)",
83
+ ),
84
+ project_root: Path | None = typer.Option(
85
+ None,
86
+ "--project-root",
87
+ help="Repo root containing .map/ (default: search upward from cwd)",
88
+ ),
89
+ state_file: Path | None = typer.Option(
90
+ None,
91
+ "--state-file",
92
+ help="Runtime waker state file (default: .map/runtime-waker-state-<persona>.json)",
93
+ ),
94
+ ) -> None:
95
+ """Show resumable session id and whether runtime waker is running."""
96
+ from cli.main import _cli_options, _print_json # lazy: avoid cycle
97
+ from cli.runtime_chat import dump_runtime_chat_status
98
+
99
+ _print_json(
100
+ dump_runtime_chat_status(
101
+ persona=persona,
102
+ project_root=project_root or _cli_options.get("project_root"),
103
+ state_file=state_file,
104
+ )
105
+ )