@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd

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 (135) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
  3. package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
  4. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  5. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  8. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  9. package/dist/resources/extensions/gsd/auto.js +10 -3
  10. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
  12. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  13. package/dist/resources/extensions/gsd/migrate-external.js +13 -4
  14. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  15. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  16. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  17. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  18. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  19. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  22. package/dist/web/standalone/.next/build-manifest.json +2 -2
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.html +1 -1
  41. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  48. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  50. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  51. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  52. package/integrations/hermes/CONTEXT.md +47 -0
  53. package/integrations/hermes/README.md +21 -0
  54. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  55. package/integrations/hermes/docs/setup.md +4 -0
  56. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  57. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  58. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  59. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  60. package/integrations/hermes/plugin.yaml +4 -0
  61. package/integrations/hermes/tests/test_config.py +6 -0
  62. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  63. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  64. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  65. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  66. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  67. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  68. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  69. package/package.json +1 -1
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/dist/session-manager.js +1 -1
  73. package/packages/daemon/dist/session-manager.js.map +1 -1
  74. package/packages/daemon/package.json +4 -4
  75. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  76. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  77. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  78. package/packages/gsd-agent-core/package.json +5 -5
  79. package/packages/gsd-agent-modes/package.json +7 -7
  80. package/packages/mcp-server/dist/server.d.ts +18 -5
  81. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/server.js +46 -5
  83. package/packages/mcp-server/dist/server.js.map +1 -1
  84. package/packages/mcp-server/dist/session-manager.js +1 -1
  85. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  86. package/packages/mcp-server/package.json +4 -4
  87. package/packages/native/package.json +1 -1
  88. package/packages/pi-agent-core/package.json +1 -1
  89. package/packages/pi-ai/package.json +1 -1
  90. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  91. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  95. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +7 -7
  97. package/packages/pi-tui/package.json +2 -2
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
  101. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
  102. package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
  103. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  104. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  105. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  106. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  107. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  108. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  109. package/src/resources/extensions/gsd/auto.ts +9 -3
  110. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  111. package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
  112. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  113. package/src/resources/extensions/gsd/migrate-external.ts +14 -4
  114. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  115. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  116. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  117. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  118. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  119. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  120. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
  121. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  123. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  124. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  125. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  126. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  127. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  128. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  129. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  130. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  131. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  132. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  133. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  134. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
@@ -0,0 +1,313 @@
1
+ """Tests for /gsd new-milestone command routing and the cross-command guards.
2
+
3
+ Covers the issue #1162 design:
4
+ - _cmd_new_milestone: input parsing (bare text, --file, empty, --auto rejected),
5
+ co-run guard (reject if auto RUNNING/BLOCKED), spawn + ack, help text.
6
+ - _cmd_auto: symmetric co-run guard (reject if milestone run active).
7
+ - _cmd_cancel: routes to cancel_milestone() when a milestone subprocess is active.
8
+ - _cmd_reply: routes planning-blocker replies to client stdin when milestone
9
+ subprocess is active with a pending blocker.
10
+
11
+ Async handlers are driven with asyncio.run, matching the convention in
12
+ test_register.py.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from pathlib import Path
19
+ from unittest.mock import MagicMock
20
+
21
+ import pytest
22
+
23
+ from open_gsd_hermes.commands import GsdCommandRouter
24
+ from open_gsd_hermes.config import GsdConfig
25
+ from open_gsd_hermes.supervisor import SupervisorContext, SupervisorState
26
+ from open_gsd_hermes.types import BindingContext
27
+
28
+
29
+ def _make_router(
30
+ *,
31
+ project_dir: str | None = "/proj",
32
+ supervisor_state: SupervisorState = SupervisorState.IDLE,
33
+ milestone_active: bool = False,
34
+ milestone_blocker_id: str | None = None,
35
+ ) -> tuple[GsdCommandRouter, MagicMock]:
36
+ """Build a router with a MagicMock client + bind store for isolated testing."""
37
+ config = GsdConfig(default_project=project_dir)
38
+ client = MagicMock()
39
+ client.milestone_active.return_value = milestone_active
40
+ client.milestone_session_id.return_value = "S-1" if milestone_active else None
41
+ client.milestone_pending_blocker_id.return_value = milestone_blocker_id
42
+ bind_store = MagicMock()
43
+ supervisor = MagicMock()
44
+ notifications = MagicMock()
45
+
46
+ sup_ctx_holder: dict[str, SupervisorContext] = {
47
+ "ctx": SupervisorContext(
48
+ session_id=None,
49
+ project_dir=project_dir,
50
+ state=supervisor_state,
51
+ )
52
+ }
53
+
54
+ def get_session_key() -> str:
55
+ return "agent:main:cli:direct:local"
56
+
57
+ def get_binding_ctx() -> BindingContext:
58
+ return BindingContext(cwd=project_dir)
59
+
60
+ def get_supervisor_ctx() -> SupervisorContext:
61
+ return sup_ctx_holder["ctx"]
62
+
63
+ def set_supervisor_ctx(ctx: SupervisorContext) -> None:
64
+ sup_ctx_holder["ctx"] = ctx
65
+
66
+ def get_platform_channel() -> tuple[str | None, str | None]:
67
+ return "cli", "local"
68
+
69
+ router = GsdCommandRouter(
70
+ config,
71
+ client,
72
+ bind_store,
73
+ supervisor,
74
+ get_session_key,
75
+ get_binding_ctx,
76
+ get_supervisor_ctx,
77
+ set_supervisor_ctx,
78
+ get_platform_channel,
79
+ notifications,
80
+ )
81
+ return router, client
82
+
83
+
84
+ @pytest.fixture
85
+ def project(tmp_path: Path) -> Path:
86
+ """A directory that looks like a GSD project (.gsd/ present)."""
87
+ (tmp_path / ".gsd").mkdir()
88
+ return tmp_path
89
+
90
+
91
+ def run(router: GsdCommandRouter, args: str) -> str:
92
+ return asyncio.run(router.handle(args))
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # _cmd_new_milestone — input parsing & usage
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ def test_new_milestone_bare_text_passes_context_text(project: Path) -> None:
101
+ router, client = _make_router(project_dir=str(project))
102
+ result = run(router, "new-milestone Build a REST API with auth")
103
+ client.create_milestone.assert_called_once()
104
+ kwargs = client.create_milestone.call_args.kwargs
105
+ assert kwargs.get("context_text") == "Build a REST API with auth"
106
+ assert kwargs.get("context_file") is None
107
+ assert "started" in result.lower() or "S-1" in result
108
+
109
+
110
+ def test_new_milestone_terminal_callback_clears_local_session_id(
111
+ project: Path,
112
+ ) -> None:
113
+ router, client = _make_router(project_dir=str(project))
114
+ client.create_milestone.return_value = "milestone-4242"
115
+
116
+ run(router, "new-milestone Build a REST API with auth")
117
+ ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
118
+ ctx.pending_blocker_id = "blk-1"
119
+ assert ctx.session_id == "milestone-4242"
120
+
121
+ on_terminal = client.create_milestone.call_args.kwargs["on_terminal"]
122
+ on_terminal("complete")
123
+
124
+ ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
125
+ assert ctx.state == SupervisorState.COMPLETE
126
+ assert ctx.session_id is None
127
+ assert ctx.pending_blocker_id is None
128
+ assert ctx.notified_terminal is True
129
+
130
+ result = run(router, "reply stale response")
131
+ client.resolve_blocker.assert_not_called()
132
+ assert "no active" in result.lower()
133
+
134
+
135
+ def test_new_milestone_file_flag_passes_context_file(project: Path) -> None:
136
+ router, client = _make_router(project_dir=str(project))
137
+ spec = project / "spec.md"
138
+ spec.write_text("milestone spec", encoding="utf-8")
139
+ run(router, f"new-milestone --file {spec}")
140
+ kwargs = client.create_milestone.call_args.kwargs
141
+ assert kwargs.get("context_file") == str(spec)
142
+ assert kwargs.get("context_text") is None
143
+
144
+
145
+ def test_new_milestone_file_flag_resolves_relative_path_from_project(
146
+ project: Path,
147
+ ) -> None:
148
+ router, client = _make_router(project_dir=str(project))
149
+ spec = project / "spec.md"
150
+ spec.write_text("milestone spec", encoding="utf-8")
151
+ run(router, "new-milestone --file spec.md")
152
+ kwargs = client.create_milestone.call_args.kwargs
153
+ assert kwargs.get("context_file") == str(spec)
154
+ assert kwargs.get("context_text") is None
155
+
156
+
157
+ def test_new_milestone_file_flag_rejects_missing_file(project: Path) -> None:
158
+ router, client = _make_router(project_dir=str(project))
159
+ result = run(router, "new-milestone --file missing.md")
160
+ assert "not found" in result.lower()
161
+ client.create_milestone.assert_not_called()
162
+
163
+
164
+ def test_new_milestone_empty_returns_usage(project: Path) -> None:
165
+ router, client = _make_router(project_dir=str(project))
166
+ result = run(router, "new-milestone")
167
+ assert "usage" in result.lower()
168
+ client.create_milestone.assert_not_called()
169
+
170
+
171
+ def test_new_milestone_rejects_auto_flag(project: Path) -> None:
172
+ """--auto chaining is out of scope; execution stays with /gsd auto."""
173
+ router, client = _make_router(project_dir=str(project))
174
+ result = run(router, "new-milestone --auto some spec")
175
+ assert "auto" in result.lower()
176
+ client.create_milestone.assert_not_called()
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Co-run guards (Q2 + Q9 symmetry)
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def test_new_milestone_rejects_when_auto_running(project: Path) -> None:
185
+ """Co-run guard: refuse if an auto session is RUNNING or BLOCKED."""
186
+ router, client = _make_router(
187
+ project_dir=str(project),
188
+ supervisor_state=SupervisorState.RUNNING,
189
+ )
190
+ result = run(router, "new-milestone some spec")
191
+ assert "cancel" in result.lower() or "running" in result.lower()
192
+ client.create_milestone.assert_not_called()
193
+
194
+
195
+ def test_new_milestone_rejects_when_auto_blocked(project: Path) -> None:
196
+ router, client = _make_router(
197
+ project_dir=str(project),
198
+ supervisor_state=SupervisorState.BLOCKED,
199
+ )
200
+ result = run(router, "new-milestone some spec")
201
+ assert "cancel" in result.lower() or "running" in result.lower()
202
+ client.create_milestone.assert_not_called()
203
+
204
+
205
+ def test_new_milestone_prioritizes_milestone_guard_over_auto_state(
206
+ project: Path,
207
+ ) -> None:
208
+ """A duplicate milestone command should report the milestone guard."""
209
+ router, client = _make_router(
210
+ project_dir=str(project),
211
+ supervisor_state=SupervisorState.RUNNING,
212
+ milestone_active=True,
213
+ )
214
+ result = run(router, "new-milestone some spec")
215
+ assert "milestone creation is in progress" in result.lower()
216
+ assert "auto is running" not in result.lower()
217
+ client.create_milestone.assert_not_called()
218
+
219
+
220
+ def test_new_milestone_allows_when_auto_complete(project: Path) -> None:
221
+ """A terminal auto state does not block milestone creation."""
222
+ router, client = _make_router(
223
+ project_dir=str(project),
224
+ supervisor_state=SupervisorState.COMPLETE,
225
+ )
226
+ run(router, "new-milestone some spec")
227
+ client.create_milestone.assert_called_once()
228
+
229
+
230
+ def test_auto_rejects_when_milestone_active(project: Path) -> None:
231
+ """Symmetric guard: /gsd auto refuses if a milestone run is in progress."""
232
+ router, client = _make_router(
233
+ project_dir=str(project),
234
+ milestone_active=True,
235
+ )
236
+ result = run(router, "auto")
237
+ assert "cancel" in result.lower() or "milestone" in result.lower()
238
+ client.execute.assert_not_called()
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # _cmd_cancel — milestone branch
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ def test_cancel_routes_to_cancel_milestone_when_milestone_active(
247
+ project: Path,
248
+ ) -> None:
249
+ router, client = _make_router(
250
+ project_dir=str(project),
251
+ milestone_active=True,
252
+ )
253
+ run(router, "cancel")
254
+ client.cancel_milestone.assert_called_once()
255
+
256
+
257
+ def test_cancel_routes_to_mcp_cancel_when_only_auto_active(project: Path) -> None:
258
+ """When no milestone proc is active, cancel falls back to the MCP path."""
259
+ router, client = _make_router(
260
+ project_dir=str(project),
261
+ supervisor_state=SupervisorState.RUNNING,
262
+ )
263
+ # Give the auto session an id so the MCP cancel-by-session path is taken
264
+ run(router, "cancel")
265
+ client.cancel_milestone.assert_not_called()
266
+ assert client.cancel.called or client.cancel_by_project.called
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # _cmd_reply — milestone-blocker branch
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def test_reply_routes_to_milestone_blocker_when_milestone_pending(
275
+ project: Path,
276
+ ) -> None:
277
+ router, client = _make_router(
278
+ project_dir=str(project),
279
+ milestone_active=True,
280
+ milestone_blocker_id="blk-9",
281
+ )
282
+ result = run(router, "reply use option B")
283
+ client.respond_to_milestone_blocker.assert_called_once_with("use option B")
284
+ assert "sent" in result.lower()
285
+
286
+
287
+ def test_reply_does_not_fall_through_to_mcp_when_milestone_active_no_blocker(
288
+ project: Path,
289
+ ) -> None:
290
+ """Active milestone without a pending blocker must not hit MCP resolve_blocker."""
291
+ router, client = _make_router(
292
+ project_dir=str(project),
293
+ milestone_active=True,
294
+ milestone_blocker_id=None,
295
+ )
296
+ router._get_supervisor_ctx().session_id = "milestone-1" # type: ignore[attr-defined]
297
+ result = run(router, "reply some answer")
298
+ client.respond_to_milestone_blocker.assert_not_called()
299
+ client.resolve_blocker.assert_not_called()
300
+ assert "no pending blocker" in result.lower()
301
+
302
+
303
+ def test_reply_falls_back_to_mcp_when_no_milestone(project: Path) -> None:
304
+ """No milestone proc → existing MCP resolve_blocker path (needs a session)."""
305
+ router, client = _make_router(
306
+ project_dir=str(project),
307
+ supervisor_state=SupervisorState.BLOCKED,
308
+ )
309
+ # set a session id so the existing reply path doesn't short-circuit
310
+ router._get_supervisor_ctx().session_id = "auto-1" # type: ignore[attr-defined]
311
+ run(router, "reply some answer")
312
+ client.respond_to_milestone_blocker.assert_not_called()
313
+ client.resolve_blocker.assert_called_once()
@@ -138,6 +138,7 @@ def test_auto_resets_session_specific_supervisor_context(tmp_path) -> None:
138
138
  )
139
139
  supervisor = MockSupervisor()
140
140
  client = MagicMock()
141
+ client.milestone_active.return_value = False
141
142
  client.execute.return_value = {"sessionId": "new-session"}
142
143
 
143
144
  router = GsdCommandRouter(
@@ -179,6 +180,7 @@ def test_status_returns_friendly_message_when_mcp_progress_fails(tmp_path) -> No
179
180
  def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
180
181
  supervisor = MockSupervisor()
181
182
  client = MagicMock()
183
+ client.milestone_active.return_value = False
182
184
  client.execute.side_effect = McpProtocolError("sidecar unavailable")
183
185
  router, _project_dir, _supervisor, _ctx, _stored = build_router(
184
186
  tmp_path, client, supervisor=supervisor
@@ -193,6 +195,7 @@ def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
193
195
  def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
194
196
  supervisor = MockSupervisor()
195
197
  client = MagicMock()
198
+ client.milestone_active.return_value = False
196
199
  client.execute.return_value = {}
197
200
  router, _project_dir, _supervisor, ctx, _stored = build_router(
198
201
  tmp_path, client, supervisor=supervisor
@@ -209,6 +212,7 @@ def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
209
212
  def test_cancel_stops_supervisor_when_mcp_cancel_fails(tmp_path) -> None:
210
213
  supervisor = MockSupervisor()
211
214
  client = MagicMock()
215
+ client.milestone_active.return_value = False
212
216
  client.cancel_by_project.side_effect = McpProtocolError("sidecar unavailable")
213
217
  router, _project_dir, _supervisor, ctx, stored = build_router(
214
218
  tmp_path, client, supervisor=supervisor
@@ -226,6 +230,7 @@ def test_cancel_sends_terminal_notification_after_successful_mcp_cancel(tmp_path
226
230
  supervisor = MockSupervisor()
227
231
  notifications = MagicMock()
228
232
  client = MagicMock()
233
+ client.milestone_active.return_value = False
229
234
  ctx = SupervisorContext(
230
235
  session_id="s1",
231
236
  state=SupervisorState.RUNNING,
@@ -261,6 +266,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
261
266
  (running_project / ".gsd").mkdir()
262
267
  supervisor = MockSupervisor()
263
268
  client = MagicMock()
269
+ client.milestone_active.return_value = False
264
270
  ctx = SupervisorContext(
265
271
  session_id="s1",
266
272
  project_dir=str(running_project),
@@ -291,6 +297,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
291
297
  def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None:
292
298
  supervisor = MockSupervisor()
293
299
  client = MagicMock()
300
+ client.milestone_active.return_value = False
294
301
  ctx = SupervisorContext(
295
302
  session_id="s1",
296
303
  project_dir=str(tmp_path / "missing"),
@@ -323,6 +330,7 @@ def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None
323
330
 
324
331
  def test_reply_returns_friendly_message_when_mcp_resolve_fails(tmp_path) -> None:
325
332
  client = MagicMock()
333
+ client.milestone_active.return_value = False
326
334
  client.resolve_blocker.side_effect = McpProtocolError("sidecar unavailable")
327
335
  ctx = SupervisorContext(session_id="s1", project_dir="/tmp/project")
328
336
  router, _project_dir, _supervisor, _ctx, _stored = build_router(
@@ -370,6 +378,38 @@ def test_supervisor_detects_blocker() -> None:
370
378
  assert any("blocker" in t.lower() or "Approve" in t for t in sent)
371
379
 
372
380
 
381
+ def test_notify_blocker_prefers_title_over_generic_message() -> None:
382
+ sent: list[str] = []
383
+
384
+ def dispatch(_name: str, args: dict) -> None:
385
+ sent.append(args.get("text", ""))
386
+
387
+ notifications = NotificationService(
388
+ MagicMock(),
389
+ GsdConfig(),
390
+ lambda: DeliveryTarget("slack", "channel", "C123"),
391
+ dispatch=dispatch,
392
+ )
393
+
394
+ notifications.notify_blocker(
395
+ SessionStatus(
396
+ status="blocked",
397
+ pending_blocker={
398
+ "type": "extension_ui_request",
399
+ "method": "select",
400
+ "id": "sel-1",
401
+ "title": "Choose milestone scope",
402
+ "message": "Action required",
403
+ "options": ["A", "B"],
404
+ },
405
+ )
406
+ )
407
+
408
+ assert len(sent) == 1
409
+ assert "GSD blocker: Choose milestone scope" in sent[0]
410
+ assert "GSD blocker: Action required" not in sent[0]
411
+
412
+
373
413
  def test_supervisor_terminal_tick_updates_progress_before_stopping() -> None:
374
414
  ctx = SupervisorContext(
375
415
  session_id="s1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-pi",
3
- "version": "1.4.0-dev.5fc22c6f",
3
+ "version": "1.4.0-dev.c18009cd",
4
4
  "description": "GSD Pi local-first coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/cloud-mcp-gateway",
3
- "version": "1.4.0-dev.5fc22c6f",
3
+ "version": "1.4.0-dev.c18009cd",
4
4
  "description": "Cloud MCP gateway for brokering remote MCP clients to local GSD runtimes",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.27.1",
32
- "@opengsd/mcp-server": "^1.4.0-dev.5fc22c6f",
32
+ "@opengsd/mcp-server": "^1.4.0-dev.c18009cd",
33
33
  "ws": "^8.20.0",
34
34
  "zod": "^4.0.0"
35
35
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/contracts",
3
- "version": "1.4.0-dev.5fc22c6f",
3
+ "version": "1.4.0-dev.c18009cd",
4
4
  "description": "Shared public contracts for GSD workspace boundaries",
5
5
  "license": "MIT",
6
6
  "gsd": {
@@ -97,7 +97,7 @@ export class SessionManager extends EventEmitter {
97
97
  }
98
98
  }
99
99
  const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();
100
- const args = ['--mode', 'rpc'];
100
+ const args = [];
101
101
  if (options.model)
102
102
  args.push('--model', options.model);
103
103
  if (options.bare)
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.\n *\n * Extends EventEmitter to emit typed session lifecycle events.\n * One active session per projectDir. Tracks events in a ring buffer,\n * detects blockers, tracks terminal state, and accumulates cost using\n * the cumulative-max pattern (K004).\n *\n * Adapted from packages/mcp-server/src/session-manager.ts with:\n * - Logger integration for structured logging\n * - EventEmitter for session lifecycle events\n * - getAllSessions() for cross-project status (R035)\n * - projectName field on ManagedSession\n */\n\nimport { execSync } from 'node:child_process';\nimport { basename, resolve } from 'node:path';\nimport { EventEmitter } from 'node:events';\nimport { RpcClient } from '@opengsd/rpc-client';\nimport type { RpcCostUpdateEvent, RpcExtensionUIRequest, RpcInitResult, SdkAgentEvent } from '@opengsd/contracts';\nimport type {\n ManagedSession,\n StartSessionOptions,\n PendingBlocker,\n} from './types.js';\nimport { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';\nimport type { Logger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Inlined detection logic (from headless-events.ts — no internal package imports)\n// ---------------------------------------------------------------------------\n\nconst FIRE_AND_FORGET_METHODS = new Set([\n 'notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text',\n]);\n\nconst TERMINAL_PREFIXES = [\n 'auto-mode stopped',\n 'step-mode stopped',\n 'auto-mode complete',\n 'no active milestone',\n 'auto-mode idle',\n];\n\nfunction isTerminalNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));\n}\n\nfunction isBlockedNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return message.includes('blocked:');\n}\n\nfunction isBlockingUIRequest(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request') return false;\n const method = String(event.method ?? '');\n return !FIRE_AND_FORGET_METHODS.has(method);\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\n/** Terminal lifecycle states — work is finished, process may be reclaimed. */\nconst TERMINAL_STATUSES: ReadonlySet<string> = new Set(['completed', 'error', 'cancelled']);\n\n/**\n * How many terminal (completed/cancelled/errored) sessions to retain for\n * inspection before evicting the oldest. Bounds heap growth on a daemon that\n * services many distinct project dirs over its lifetime — without this, every\n * session ever run stays resident forever (events buffer + RpcClient ref).\n */\nconst MAX_TERMINAL_SESSIONS = 50;\n\nexport class SessionManager extends EventEmitter {\n /** Sessions keyed by resolved projectDir for duplicate-start prevention */\n private sessions = new Map<string, ManagedSession>();\n\n /** Resolved dirs of terminal sessions, oldest-first — for bounded retention. */\n private terminalOrder: string[] = [];\n\n constructor(private readonly logger: Logger) {\n super();\n }\n\n /**\n * Start a new GSD auto-mode session for the given project directory.\n *\n * Rejects if a session already exists for this projectDir.\n * Creates an RpcClient, starts the process, performs the v2 init handshake,\n * wires event tracking, and sends '/gsd auto' to begin execution.\n */\n async startSession(options: StartSessionOptions): Promise<string> {\n const { projectDir } = options;\n\n if (!projectDir || projectDir.trim() === '') {\n throw new Error('projectDir is required and cannot be empty');\n }\n\n const resolvedDir = resolve(projectDir);\n const projectName = basename(resolvedDir);\n\n const existing = this.sessions.get(resolvedDir);\n if (existing) {\n // A terminal session (completed/cancelled/error) is retained only for\n // inspection — it must not block a fresh start for the same dir. Evict it\n // and reclaim its process before starting anew.\n if (TERMINAL_STATUSES.has(existing.status)) {\n this.evictSession(resolvedDir);\n } else {\n throw new Error(\n `Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`\n );\n }\n }\n\n const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();\n\n const args: string[] = ['--mode', 'rpc'];\n if (options.model) args.push('--model', options.model);\n if (options.bare) args.push('--bare');\n\n const client = new RpcClient({\n cliPath,\n cwd: resolvedDir,\n args,\n });\n\n // Build the session shell before async operations so we can track state\n const session: ManagedSession = {\n sessionId: '', // filled after init\n projectDir: resolvedDir,\n projectName,\n status: 'starting',\n client,\n events: [],\n pendingBlocker: null,\n cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },\n startTime: Date.now(),\n };\n\n // Insert into map early (keyed by dir) so concurrent starts are rejected\n this.sessions.set(resolvedDir, session);\n\n try {\n // Start the process with timeout\n await Promise.race([\n client.start(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.start() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]);\n\n // Perform v2 init handshake\n const initResult: RpcInitResult = await Promise.race([\n client.init(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.init() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]) as RpcInitResult;\n\n session.sessionId = initResult.sessionId;\n session.status = 'running';\n\n // Wire event tracking\n session.unsubscribe = client.onEvent((event: SdkAgentEvent) => {\n this.handleEvent(session, event);\n });\n\n // Kick off auto-mode\n const command = options.command ?? '/gsd auto';\n await client.prompt(command);\n\n this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });\n this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });\n\n return session.sessionId;\n } catch (err) {\n session.status = 'error';\n session.error = err instanceof Error ? err.message : String(err);\n\n // Attempt cleanup\n try { await client.stop(); } catch { /* swallow cleanup errors */ }\n\n this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });\n this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });\n\n // Keep session in map so callers can inspect the error, but under the\n // bounded terminal-retention policy so it is eventually reclaimed.\n this.markTerminal(resolvedDir);\n throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);\n }\n }\n\n /**\n * Look up a session by sessionId.\n * Linear scan is fine — we expect <10 concurrent sessions.\n */\n getSession(sessionId: string): ManagedSession | undefined {\n for (const session of this.sessions.values()) {\n if (session.sessionId === sessionId) return session;\n }\n return undefined;\n }\n\n /**\n * Look up a session by project directory (direct map lookup).\n */\n getSessionByDir(projectDir: string): ManagedSession | undefined {\n return this.sessions.get(resolve(projectDir));\n }\n\n /**\n * Return all tracked sessions (R035 — cross-project status).\n */\n getAllSessions(): ManagedSession[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Resolve a pending blocker by sending a UI response.\n */\n async resolveBlocker(sessionId: string, response: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (!session.pendingBlocker) throw new Error(`No pending blocker for session ${sessionId}`);\n\n const blocker = session.pendingBlocker;\n session.client.sendUIResponse(blocker.id, { value: response });\n session.pendingBlocker = null;\n if (session.status === 'blocked') {\n session.status = 'running';\n }\n\n this.logger.info('blocker resolved', {\n sessionId,\n projectDir: session.projectDir,\n blockerId: blocker.id,\n blockerMethod: blocker.method,\n });\n }\n\n /**\n * Cancel a running session — abort current operation then stop the process.\n */\n async cancelSession(sessionId: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n try {\n await session.client.abort();\n } catch { /* may already be stopped */ }\n\n try {\n await session.client.stop();\n } catch { /* swallow */ }\n\n session.status = 'cancelled';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n\n this.logger.info('session cancelled', { sessionId, projectDir: session.projectDir });\n\n // Symmetric teardown: notify listeners (EventBridge) so they can dispose the\n // session's batcher, channel mappings, and any blocker collectors — the\n // start path emits 'session:started', so cancel must emit too.\n this.emit('session:cancelled', {\n sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n\n this.markTerminal(session.projectDir);\n }\n\n async cancelSessionByDir(projectDir: string): Promise<void> {\n const session = this.getSessionByDir(projectDir);\n if (!session) throw new Error(`Session not found for projectDir: ${projectDir}`);\n await this.cancelSession(session.sessionId);\n }\n\n /**\n * Build a HeadlessJsonResult-shaped object from accumulated session state.\n */\n getResult(sessionId: string): Record<string, unknown> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n const durationMs = Date.now() - session.startTime;\n\n return {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n status: session.status,\n durationMs,\n cost: session.cost,\n recentEvents: session.events.slice(-10),\n pendingBlocker: session.pendingBlocker\n ? { id: session.pendingBlocker.id, method: session.pendingBlocker.method, message: session.pendingBlocker.message }\n : null,\n error: session.error ?? null,\n };\n }\n\n /**\n * Stop all active sessions and clean up resources.\n */\n async cleanup(): Promise<void> {\n const stopPromises: Promise<void>[] = [];\n\n for (const session of this.sessions.values()) {\n session.unsubscribe?.();\n if (session.status === 'running' || session.status === 'starting' || session.status === 'blocked') {\n stopPromises.push(\n session.client.stop().catch(() => { /* swallow */ })\n );\n session.status = 'cancelled';\n }\n }\n\n await Promise.allSettled(stopPromises);\n\n this.sessions.clear();\n this.terminalOrder = [];\n }\n\n // ---------------------------------------------------------------------------\n // Private: Terminal-session retention (bounded)\n // ---------------------------------------------------------------------------\n\n /**\n * Record a session (by resolved dir) as terminal and evict the oldest\n * terminal sessions once the retention cap is exceeded. Retains recent\n * terminal sessions for inspection while bounding heap growth.\n */\n private markTerminal(resolvedDir: string): void {\n // Move to most-recent position (dedupe if it was already terminal).\n const existingIdx = this.terminalOrder.indexOf(resolvedDir);\n if (existingIdx !== -1) this.terminalOrder.splice(existingIdx, 1);\n this.terminalOrder.push(resolvedDir);\n\n while (this.terminalOrder.length > MAX_TERMINAL_SESSIONS) {\n const oldest = this.terminalOrder.shift();\n if (oldest) this.evictSession(oldest);\n }\n }\n\n /**\n * Remove a session from tracking and best-effort reclaim its agent process.\n */\n private evictSession(resolvedDir: string): void {\n const session = this.sessions.get(resolvedDir);\n this.sessions.delete(resolvedDir);\n const orderIdx = this.terminalOrder.indexOf(resolvedDir);\n if (orderIdx !== -1) this.terminalOrder.splice(orderIdx, 1);\n\n if (session) {\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n // The agent child process may still be alive (e.g. a completed session\n // kept for follow-up relay) — reclaim it on eviction.\n void session.client.stop().catch(() => { /* swallow */ });\n }\n }\n\n /**\n * Resolve the GSD CLI path.\n *\n * 1. GSD_CLI_PATH env var (highest priority)\n * 2. `which gsd` → resolve to the actual dist/cli.js\n */\n static resolveCLIPath(): string {\n const envPath = process.env['GSD_CLI_PATH'];\n if (envPath) return resolve(envPath);\n\n try {\n const gsdBin = execSync('which gsd', { encoding: 'utf-8' }).trim();\n if (gsdBin) return resolve(gsdBin);\n } catch {\n // which failed\n }\n\n throw new Error(\n 'Cannot find GSD CLI. Set GSD_CLI_PATH environment variable or ensure `gsd` is in PATH.'\n );\n }\n\n // ---------------------------------------------------------------------------\n // Private: Event Handling\n // ---------------------------------------------------------------------------\n\n private handleEvent(session: ManagedSession, event: SdkAgentEvent): void {\n // Ring buffer: push and trim\n session.events.push(event);\n if (session.events.length > MAX_EVENTS) {\n session.events.splice(0, session.events.length - MAX_EVENTS);\n }\n\n // Forward event to listeners\n this.logger.debug('session event', { sessionId: session.sessionId, type: (event as Record<string, unknown>).type as string });\n this.emit('session:event', { sessionId: session.sessionId, projectDir: session.projectDir, event });\n\n // Cost tracking (K004 — cumulative-max)\n if ((event as Record<string, unknown>).type === 'cost_update') {\n const costEvent = event as unknown as RpcCostUpdateEvent;\n session.cost.totalCost = Math.max(session.cost.totalCost, costEvent.cumulativeCost ?? 0);\n if (costEvent.tokens) {\n session.cost.tokens.input = Math.max(session.cost.tokens.input, costEvent.tokens.input ?? 0);\n session.cost.tokens.output = Math.max(session.cost.tokens.output, costEvent.tokens.output ?? 0);\n session.cost.tokens.cacheRead = Math.max(session.cost.tokens.cacheRead, costEvent.tokens.cacheRead ?? 0);\n session.cost.tokens.cacheWrite = Math.max(session.cost.tokens.cacheWrite, costEvent.tokens.cacheWrite ?? 0);\n }\n }\n\n // Terminal detection — auto-mode/step-mode stopped\n if (isTerminalNotification(event as Record<string, unknown>)) {\n if (isBlockedNotification(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n } else {\n session.status = 'completed';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n this.logger.info('session completed', { sessionId: session.sessionId, projectDir: session.projectDir });\n this.emit('session:completed', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n this.markTerminal(session.projectDir);\n }\n return;\n }\n\n // Blocker detection — non-fire-and-forget extension_ui_request\n if (isBlockingUIRequest(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction timeout(ms: number, message: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(message)), ms);\n });\n}\n\nfunction extractBlocker(event: SdkAgentEvent): PendingBlocker {\n const uiEvent = event as unknown as RpcExtensionUIRequest;\n return {\n id: String(uiEvent.id ?? ''),\n method: uiEvent.method,\n message: String((uiEvent as Record<string, unknown>).title ?? (uiEvent as Record<string, unknown>).message ?? ''),\n event: uiEvent,\n };\n}\n"]}
1
+ {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SessionManager — manages RpcClient lifecycle for daemon-driven GSD execution.\n *\n * Extends EventEmitter to emit typed session lifecycle events.\n * One active session per projectDir. Tracks events in a ring buffer,\n * detects blockers, tracks terminal state, and accumulates cost using\n * the cumulative-max pattern (K004).\n *\n * Adapted from packages/mcp-server/src/session-manager.ts with:\n * - Logger integration for structured logging\n * - EventEmitter for session lifecycle events\n * - getAllSessions() for cross-project status (R035)\n * - projectName field on ManagedSession\n */\n\nimport { execSync } from 'node:child_process';\nimport { basename, resolve } from 'node:path';\nimport { EventEmitter } from 'node:events';\nimport { RpcClient } from '@opengsd/rpc-client';\nimport type { RpcCostUpdateEvent, RpcExtensionUIRequest, RpcInitResult, SdkAgentEvent } from '@opengsd/contracts';\nimport type {\n ManagedSession,\n StartSessionOptions,\n PendingBlocker,\n} from './types.js';\nimport { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';\nimport type { Logger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Inlined detection logic (from headless-events.ts — no internal package imports)\n// ---------------------------------------------------------------------------\n\nconst FIRE_AND_FORGET_METHODS = new Set([\n 'notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text',\n]);\n\nconst TERMINAL_PREFIXES = [\n 'auto-mode stopped',\n 'step-mode stopped',\n 'auto-mode complete',\n 'no active milestone',\n 'auto-mode idle',\n];\n\nfunction isTerminalNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));\n}\n\nfunction isBlockedNotification(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request' || event.method !== 'notify') return false;\n const message = String(event.message ?? '').toLowerCase();\n return message.includes('blocked:');\n}\n\nfunction isBlockingUIRequest(event: Record<string, unknown>): boolean {\n if (event.type !== 'extension_ui_request') return false;\n const method = String(event.method ?? '');\n return !FIRE_AND_FORGET_METHODS.has(method);\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\n/** Terminal lifecycle states — work is finished, process may be reclaimed. */\nconst TERMINAL_STATUSES: ReadonlySet<string> = new Set(['completed', 'error', 'cancelled']);\n\n/**\n * How many terminal (completed/cancelled/errored) sessions to retain for\n * inspection before evicting the oldest. Bounds heap growth on a daemon that\n * services many distinct project dirs over its lifetime — without this, every\n * session ever run stays resident forever (events buffer + RpcClient ref).\n */\nconst MAX_TERMINAL_SESSIONS = 50;\n\nexport class SessionManager extends EventEmitter {\n /** Sessions keyed by resolved projectDir for duplicate-start prevention */\n private sessions = new Map<string, ManagedSession>();\n\n /** Resolved dirs of terminal sessions, oldest-first — for bounded retention. */\n private terminalOrder: string[] = [];\n\n constructor(private readonly logger: Logger) {\n super();\n }\n\n /**\n * Start a new GSD auto-mode session for the given project directory.\n *\n * Rejects if a session already exists for this projectDir.\n * Creates an RpcClient, starts the process, performs the v2 init handshake,\n * wires event tracking, and sends '/gsd auto' to begin execution.\n */\n async startSession(options: StartSessionOptions): Promise<string> {\n const { projectDir } = options;\n\n if (!projectDir || projectDir.trim() === '') {\n throw new Error('projectDir is required and cannot be empty');\n }\n\n const resolvedDir = resolve(projectDir);\n const projectName = basename(resolvedDir);\n\n const existing = this.sessions.get(resolvedDir);\n if (existing) {\n // A terminal session (completed/cancelled/error) is retained only for\n // inspection — it must not block a fresh start for the same dir. Evict it\n // and reclaim its process before starting anew.\n if (TERMINAL_STATUSES.has(existing.status)) {\n this.evictSession(resolvedDir);\n } else {\n throw new Error(\n `Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`\n );\n }\n }\n\n const cliPath = options.cliPath ?? SessionManager.resolveCLIPath();\n\n const args: string[] = [];\n if (options.model) args.push('--model', options.model);\n if (options.bare) args.push('--bare');\n\n const client = new RpcClient({\n cliPath,\n cwd: resolvedDir,\n args,\n });\n\n // Build the session shell before async operations so we can track state\n const session: ManagedSession = {\n sessionId: '', // filled after init\n projectDir: resolvedDir,\n projectName,\n status: 'starting',\n client,\n events: [],\n pendingBlocker: null,\n cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },\n startTime: Date.now(),\n };\n\n // Insert into map early (keyed by dir) so concurrent starts are rejected\n this.sessions.set(resolvedDir, session);\n\n try {\n // Start the process with timeout\n await Promise.race([\n client.start(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.start() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]);\n\n // Perform v2 init handshake\n const initResult: RpcInitResult = await Promise.race([\n client.init(),\n timeout(INIT_TIMEOUT_MS, `RpcClient.init() timed out after ${INIT_TIMEOUT_MS}ms`),\n ]) as RpcInitResult;\n\n session.sessionId = initResult.sessionId;\n session.status = 'running';\n\n // Wire event tracking\n session.unsubscribe = client.onEvent((event: SdkAgentEvent) => {\n this.handleEvent(session, event);\n });\n\n // Kick off auto-mode\n const command = options.command ?? '/gsd auto';\n await client.prompt(command);\n\n this.logger.info('session started', { sessionId: session.sessionId, projectDir: resolvedDir });\n this.emit('session:started', { sessionId: session.sessionId, projectDir: resolvedDir, projectName });\n\n return session.sessionId;\n } catch (err) {\n session.status = 'error';\n session.error = err instanceof Error ? err.message : String(err);\n\n // Attempt cleanup\n try { await client.stop(); } catch { /* swallow cleanup errors */ }\n\n this.logger.error('session error', { sessionId: session.sessionId, projectDir: resolvedDir, error: session.error });\n this.emit('session:error', { sessionId: session.sessionId, projectDir: resolvedDir, projectName, error: session.error });\n\n // Keep session in map so callers can inspect the error, but under the\n // bounded terminal-retention policy so it is eventually reclaimed.\n this.markTerminal(resolvedDir);\n throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);\n }\n }\n\n /**\n * Look up a session by sessionId.\n * Linear scan is fine — we expect <10 concurrent sessions.\n */\n getSession(sessionId: string): ManagedSession | undefined {\n for (const session of this.sessions.values()) {\n if (session.sessionId === sessionId) return session;\n }\n return undefined;\n }\n\n /**\n * Look up a session by project directory (direct map lookup).\n */\n getSessionByDir(projectDir: string): ManagedSession | undefined {\n return this.sessions.get(resolve(projectDir));\n }\n\n /**\n * Return all tracked sessions (R035 — cross-project status).\n */\n getAllSessions(): ManagedSession[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Resolve a pending blocker by sending a UI response.\n */\n async resolveBlocker(sessionId: string, response: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (!session.pendingBlocker) throw new Error(`No pending blocker for session ${sessionId}`);\n\n const blocker = session.pendingBlocker;\n session.client.sendUIResponse(blocker.id, { value: response });\n session.pendingBlocker = null;\n if (session.status === 'blocked') {\n session.status = 'running';\n }\n\n this.logger.info('blocker resolved', {\n sessionId,\n projectDir: session.projectDir,\n blockerId: blocker.id,\n blockerMethod: blocker.method,\n });\n }\n\n /**\n * Cancel a running session — abort current operation then stop the process.\n */\n async cancelSession(sessionId: string): Promise<void> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n try {\n await session.client.abort();\n } catch { /* may already be stopped */ }\n\n try {\n await session.client.stop();\n } catch { /* swallow */ }\n\n session.status = 'cancelled';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n\n this.logger.info('session cancelled', { sessionId, projectDir: session.projectDir });\n\n // Symmetric teardown: notify listeners (EventBridge) so they can dispose the\n // session's batcher, channel mappings, and any blocker collectors — the\n // start path emits 'session:started', so cancel must emit too.\n this.emit('session:cancelled', {\n sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n\n this.markTerminal(session.projectDir);\n }\n\n async cancelSessionByDir(projectDir: string): Promise<void> {\n const session = this.getSessionByDir(projectDir);\n if (!session) throw new Error(`Session not found for projectDir: ${projectDir}`);\n await this.cancelSession(session.sessionId);\n }\n\n /**\n * Build a HeadlessJsonResult-shaped object from accumulated session state.\n */\n getResult(sessionId: string): Record<string, unknown> {\n const session = this.getSession(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n\n const durationMs = Date.now() - session.startTime;\n\n return {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n status: session.status,\n durationMs,\n cost: session.cost,\n recentEvents: session.events.slice(-10),\n pendingBlocker: session.pendingBlocker\n ? { id: session.pendingBlocker.id, method: session.pendingBlocker.method, message: session.pendingBlocker.message }\n : null,\n error: session.error ?? null,\n };\n }\n\n /**\n * Stop all active sessions and clean up resources.\n */\n async cleanup(): Promise<void> {\n const stopPromises: Promise<void>[] = [];\n\n for (const session of this.sessions.values()) {\n session.unsubscribe?.();\n if (session.status === 'running' || session.status === 'starting' || session.status === 'blocked') {\n stopPromises.push(\n session.client.stop().catch(() => { /* swallow */ })\n );\n session.status = 'cancelled';\n }\n }\n\n await Promise.allSettled(stopPromises);\n\n this.sessions.clear();\n this.terminalOrder = [];\n }\n\n // ---------------------------------------------------------------------------\n // Private: Terminal-session retention (bounded)\n // ---------------------------------------------------------------------------\n\n /**\n * Record a session (by resolved dir) as terminal and evict the oldest\n * terminal sessions once the retention cap is exceeded. Retains recent\n * terminal sessions for inspection while bounding heap growth.\n */\n private markTerminal(resolvedDir: string): void {\n // Move to most-recent position (dedupe if it was already terminal).\n const existingIdx = this.terminalOrder.indexOf(resolvedDir);\n if (existingIdx !== -1) this.terminalOrder.splice(existingIdx, 1);\n this.terminalOrder.push(resolvedDir);\n\n while (this.terminalOrder.length > MAX_TERMINAL_SESSIONS) {\n const oldest = this.terminalOrder.shift();\n if (oldest) this.evictSession(oldest);\n }\n }\n\n /**\n * Remove a session from tracking and best-effort reclaim its agent process.\n */\n private evictSession(resolvedDir: string): void {\n const session = this.sessions.get(resolvedDir);\n this.sessions.delete(resolvedDir);\n const orderIdx = this.terminalOrder.indexOf(resolvedDir);\n if (orderIdx !== -1) this.terminalOrder.splice(orderIdx, 1);\n\n if (session) {\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n // The agent child process may still be alive (e.g. a completed session\n // kept for follow-up relay) — reclaim it on eviction.\n void session.client.stop().catch(() => { /* swallow */ });\n }\n }\n\n /**\n * Resolve the GSD CLI path.\n *\n * 1. GSD_CLI_PATH env var (highest priority)\n * 2. `which gsd` → resolve to the actual dist/cli.js\n */\n static resolveCLIPath(): string {\n const envPath = process.env['GSD_CLI_PATH'];\n if (envPath) return resolve(envPath);\n\n try {\n const gsdBin = execSync('which gsd', { encoding: 'utf-8' }).trim();\n if (gsdBin) return resolve(gsdBin);\n } catch {\n // which failed\n }\n\n throw new Error(\n 'Cannot find GSD CLI. Set GSD_CLI_PATH environment variable or ensure `gsd` is in PATH.'\n );\n }\n\n // ---------------------------------------------------------------------------\n // Private: Event Handling\n // ---------------------------------------------------------------------------\n\n private handleEvent(session: ManagedSession, event: SdkAgentEvent): void {\n // Ring buffer: push and trim\n session.events.push(event);\n if (session.events.length > MAX_EVENTS) {\n session.events.splice(0, session.events.length - MAX_EVENTS);\n }\n\n // Forward event to listeners\n this.logger.debug('session event', { sessionId: session.sessionId, type: (event as Record<string, unknown>).type as string });\n this.emit('session:event', { sessionId: session.sessionId, projectDir: session.projectDir, event });\n\n // Cost tracking (K004 — cumulative-max)\n if ((event as Record<string, unknown>).type === 'cost_update') {\n const costEvent = event as unknown as RpcCostUpdateEvent;\n session.cost.totalCost = Math.max(session.cost.totalCost, costEvent.cumulativeCost ?? 0);\n if (costEvent.tokens) {\n session.cost.tokens.input = Math.max(session.cost.tokens.input, costEvent.tokens.input ?? 0);\n session.cost.tokens.output = Math.max(session.cost.tokens.output, costEvent.tokens.output ?? 0);\n session.cost.tokens.cacheRead = Math.max(session.cost.tokens.cacheRead, costEvent.tokens.cacheRead ?? 0);\n session.cost.tokens.cacheWrite = Math.max(session.cost.tokens.cacheWrite, costEvent.tokens.cacheWrite ?? 0);\n }\n }\n\n // Terminal detection — auto-mode/step-mode stopped\n if (isTerminalNotification(event as Record<string, unknown>)) {\n if (isBlockedNotification(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n } else {\n session.status = 'completed';\n session.unsubscribe?.();\n session.unsubscribe = undefined;\n this.logger.info('session completed', { sessionId: session.sessionId, projectDir: session.projectDir });\n this.emit('session:completed', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n });\n this.markTerminal(session.projectDir);\n }\n return;\n }\n\n // Blocker detection — non-fire-and-forget extension_ui_request\n if (isBlockingUIRequest(event as Record<string, unknown>)) {\n session.status = 'blocked';\n session.pendingBlocker = extractBlocker(event);\n this.logger.info('session blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n blockerId: session.pendingBlocker.id,\n blockerMethod: session.pendingBlocker.method,\n });\n this.emit('session:blocked', {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n blocker: session.pendingBlocker,\n });\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction timeout(ms: number, message: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(message)), ms);\n });\n}\n\nfunction extractBlocker(event: SdkAgentEvent): PendingBlocker {\n const uiEvent = event as unknown as RpcExtensionUIRequest;\n return {\n id: String(uiEvent.id ?? ''),\n method: uiEvent.method,\n message: String((uiEvent as Record<string, unknown>).title ?? (uiEvent as Record<string, unknown>).message ?? ''),\n event: uiEvent,\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/daemon",
3
- "version": "1.4.0-dev.5fc22c6f",
3
+ "version": "1.4.0-dev.c18009cd",
4
4
  "description": "GSD daemon — background process for project monitoring and Discord integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/sdk": "^0.52.0",
34
- "@opengsd/contracts": "^1.4.0-dev.5fc22c6f",
35
- "@opengsd/mcp-server": "^1.4.0-dev.5fc22c6f",
36
- "@opengsd/rpc-client": "^1.4.0-dev.5fc22c6f",
34
+ "@opengsd/contracts": "^1.4.0-dev.c18009cd",
35
+ "@opengsd/mcp-server": "^1.4.0-dev.c18009cd",
36
+ "@opengsd/rpc-client": "^1.4.0-dev.c18009cd",
37
37
  "discord.js": "^14.25.1",
38
38
  "ws": "^8.20.0",
39
39
  "yaml": "^2.8.0",
@@ -1 +1 @@
1
- {"version":3,"file":"agent-session-navigation.d.ts","sourceRoot":"","sources":["../../src/session/agent-session-navigation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+CAA+C,CAAC;AAOlF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AAWvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,qBAAa,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,gBAAgB;gBAAtB,IAAI,EAAE,gBAAgB;IAE3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAStD,UAAU,CAAC,OAAO,CAAC,EAAE;QAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,WAAW,CAAC;KAC1B,GAAG,OAAO,CAAC,OAAO,CAAC;IAiEd,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkEpD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAmDlF,eAAe,IAAI,MAAM;IAIzB,IAAI,QAAQ,IAAI,UAAU,GAAG,UAAU,CAEtC;IAED,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;IAI1C,YAAY,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/G,OAAO,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC;IAgK7G,yBAAyB,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBrE,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,MAAM;IAWxF,eAAe,IAAI,YAAY;IA6C/B,eAAe,IAAI,YAAY,GAAG,SAAS;IA8CrC,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBxD,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;IAiC1C,oBAAoB,IAAI,MAAM,GAAG,SAAS;CAwB1C"}
1
+ {"version":3,"file":"agent-session-navigation.d.ts","sourceRoot":"","sources":["../../src/session/agent-session-navigation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+CAA+C,CAAC;AAOlF,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AAWvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,qBAAa,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,gBAAgB;gBAAtB,IAAI,EAAE,gBAAgB;IAE3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B,qCAAqC,IAAI,OAAO,CAAC,IAAI,CAAC;IAStD,UAAU,CAAC,OAAO,CAAC,EAAE;QAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,WAAW,CAAC;KAC1B,GAAG,OAAO,CAAC,OAAO,CAAC;IAoEd,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkEpD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAmDlF,eAAe,IAAI,MAAM;IAIzB,IAAI,QAAQ,IAAI,UAAU,GAAG,UAAU,CAEtC;IAED,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;IAI1C,YAAY,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/G,OAAO,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC;IAgK7G,yBAAyB,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBrE,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,MAAM;IAWxF,eAAe,IAAI,YAAY;IA6C/B,eAAe,IAAI,YAAY,GAAG,SAAS;IA8CrC,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBxD,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;IAiC1C,oBAAoB,IAAI,MAAM,GAAG,SAAS;CAwB1C"}