pi-mono-all 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,825 @@
1
+ // Pi Team-mode — Extension Entry Point
2
+ //
3
+ // Faithful port of Claude Code's team-mode mode:
4
+ // - `agent` spawns a worker subprocess. The caller (coordinator) ends its
5
+ // turn; when the worker exits we push a <task-notification> user-role
6
+ // message to the session with triggerTurn=true so the coordinator wakes
7
+ // up event-driven, not via polling.
8
+ // - `send_message` continues an existing worker (full prior context).
9
+ // - `task_stop` terminates a running worker.
10
+ // - `task_output` reads a worker's current/latest output.
11
+ // - `task_create/update/list/get` is the TODO list (coordinator assigns
12
+ // owners via task_update; no auto-claim).
13
+ // - `team_create/delete` groups workers for bulk cleanup + isolation defaults.
14
+ // - Coordinator mode (PI_TEAM_MATE_COORDINATOR=1) injects a coordinator
15
+ // system prompt teaching the parent LLM the delegation model.
16
+ // - Teammate subprocesses get the TEAMMATE_SYSTEM_PROMPT_ADDENDUM, so they
17
+ // know to communicate via send_message rather than free text.
18
+
19
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
20
+ import { Type } from "@sinclair/typebox";
21
+
22
+ import { TeamMateStore } from "./core/store.js";
23
+ import { TaskStore } from "./core/tasks.js";
24
+ import { loadModelConfig } from "./core/model-config.js";
25
+ import {
26
+ formatTaskNotification,
27
+ getCoordinatorSystemPrompt,
28
+ isCoordinatorMode,
29
+ } from "./core/prompts.js";
30
+ import type { TeammateRunResult, TeammateStatus } from "./core/types.js";
31
+ import { AgentManager, type TeammateEndMetrics } from "./managers/agent-manager.js";
32
+ import { DelegationManager, type DelegationResult } from "./managers/delegation-manager.js";
33
+ import { TeamManager } from "./managers/team-manager.js";
34
+ import { TaskManager, VersionConflictError } from "./managers/task-manager.js";
35
+ import {
36
+ formatTaskDetails,
37
+ formatTaskLine,
38
+ formatTaskList,
39
+ formatTeamDashboard,
40
+ formatTeammateList,
41
+ formatTeammateStatus,
42
+ } from "./ui/formatters.js";
43
+ import { renderTaskNotification, type TaskNotificationDetails } from "./ui/notification-box.js";
44
+ import { startTeamMateWidget } from "./ui/widget.js";
45
+
46
+ type ParentManagers = {
47
+ agents: AgentManager;
48
+ delegations: DelegationManager;
49
+ teams: TeamManager;
50
+ tasks: TaskManager;
51
+ };
52
+
53
+ let parentManagers: ParentManagers | undefined;
54
+ let subprocessTasks: TaskManager | undefined;
55
+ let parentPi: ExtensionAPI | undefined;
56
+ let disposeWidget: (() => void) | undefined;
57
+
58
+ function isSubprocess(): boolean {
59
+ return process.env.PI_TEAM_MATE_SUBPROCESS === "1";
60
+ }
61
+
62
+ function getParentManagers(): ParentManagers {
63
+ if (!parentManagers) throw new Error("team-mode not initialized");
64
+ return parentManagers;
65
+ }
66
+
67
+ function getTaskManager(): TaskManager {
68
+ if (subprocessTasks) return subprocessTasks;
69
+ if (parentManagers) return parentManagers.tasks;
70
+ throw new Error("team-mode not initialized");
71
+ }
72
+
73
+ async function initParent(pi: ExtensionAPI, ctx: ExtensionContext): Promise<ParentManagers> {
74
+ parentPi = pi;
75
+ const store = new TeamMateStore();
76
+ const taskStore = new TaskStore();
77
+ const agents = new AgentManager({
78
+ store,
79
+ getParentSessionId: () => ctx.sessionManager.getSessionId(),
80
+ getDefaultCwd: () => ctx.cwd,
81
+ onTeammateEnd: (record, metrics) => {
82
+ void pushTaskNotification(record, metrics);
83
+ },
84
+ });
85
+ const teams = new TeamManager(store, agents, () => ctx.sessionManager.getSessionId());
86
+ const delegations = new DelegationManager(agents);
87
+ const tasks = new TaskManager({
88
+ store: taskStore,
89
+ getParentSessionId: () => ctx.sessionManager.getSessionId(),
90
+ getTaskCompletedHook: async () => (await loadModelConfig()).taskCompletedHook,
91
+ getCwd: () => ctx.cwd,
92
+ });
93
+ parentManagers = { agents, delegations, teams, tasks };
94
+ return parentManagers;
95
+ }
96
+
97
+ function initSubprocess(): TaskManager {
98
+ const parentSessionId = process.env.PI_TEAM_MATE_PARENT_SESSION_ID ?? "";
99
+ subprocessTasks = new TaskManager({
100
+ store: new TaskStore(),
101
+ getParentSessionId: () => parentSessionId,
102
+ getTaskCompletedHook: async () => (await loadModelConfig()).taskCompletedHook,
103
+ getCwd: () => process.cwd(),
104
+ });
105
+ return subprocessTasks;
106
+ }
107
+
108
+ async function refreshWidget(_ctx: ExtensionContext): Promise<void> {
109
+ // Widget is now event-driven via startTeamMateWidget + AgentManager subscriptions.
110
+ }
111
+
112
+ const STATUS_TO_CC: Record<TeammateStatus, "completed" | "failed" | "killed"> = {
113
+ completed: "completed",
114
+ stopped: "killed",
115
+ failed: "failed",
116
+ running: "failed",
117
+ pending: "failed",
118
+ };
119
+
120
+ /**
121
+ * Push a `<task-notification>` to the coordinator's session as a user-role
122
+ * message that triggers a new turn. Mirrors Claude Code's wake-up mechanism.
123
+ */
124
+ async function pushTaskNotification(
125
+ record: {
126
+ id: string;
127
+ name: string;
128
+ status: TeammateStatus;
129
+ lastResult?: string;
130
+ lastExitCode?: number;
131
+ },
132
+ metrics: TeammateEndMetrics,
133
+ ): Promise<void> {
134
+ if (!parentPi) return;
135
+ const ccStatus = STATUS_TO_CC[record.status] ?? "failed";
136
+ const summary = `Agent "${record.name}" ${ccStatus}`;
137
+ const xml = formatTaskNotification({
138
+ taskId: record.id,
139
+ status: ccStatus,
140
+ summary,
141
+ result: record.lastResult,
142
+ toolUses: metrics.toolUses,
143
+ durationMs: metrics.durationMs,
144
+ });
145
+ try {
146
+ const details: TaskNotificationDetails = {
147
+ taskId: record.id,
148
+ status: ccStatus,
149
+ durationMs: metrics.durationMs,
150
+ metrics: metrics.metrics,
151
+ transcriptPath: metrics.transcriptPath,
152
+ summary,
153
+ };
154
+ parentPi.sendMessage(
155
+ {
156
+ customType: "task-notification",
157
+ content: xml,
158
+ display: true,
159
+ details,
160
+ },
161
+ { triggerTurn: true },
162
+ );
163
+ } catch {
164
+ /* pi versions without triggerTurn support are still useful — the user
165
+ can poll /teammate list. Non-fatal. */
166
+ }
167
+ }
168
+
169
+ // --- schemas ---
170
+
171
+ const IsolationSchema = Type.Union([Type.Literal("none"), Type.Literal("worktree")]);
172
+ const RuntimeSchema = Type.Union([Type.Literal("subprocess"), Type.Literal("transient")]);
173
+ const ThinkingLevelSchema = Type.Union([
174
+ Type.Literal("off"),
175
+ Type.Literal("minimal"),
176
+ Type.Literal("low"),
177
+ Type.Literal("medium"),
178
+ Type.Literal("high"),
179
+ Type.Literal("xhigh"),
180
+ ]);
181
+ const TaskStatusSchema = Type.Union([
182
+ Type.Literal("pending"),
183
+ Type.Literal("in_progress"),
184
+ Type.Literal("completed"),
185
+ Type.Literal("failed"),
186
+ Type.Literal("deleted"),
187
+ ]);
188
+
189
+ const AgentParams = Type.Object({
190
+ description: Type.String({ description: "Short (3-5 word) task label. Shown in UIs and the task-notification summary." }),
191
+ prompt: Type.String({ description: "Self-contained task brief. Workers don't see the coordinator's conversation." }),
192
+ name: Type.Optional(Type.String({ description: "Unique teammate name. Pass as `to` in send_message to continue." })),
193
+ team_name: Type.Optional(Type.String({ description: "Team id from team_create (optional grouping)." })),
194
+ subagent_type: Type.Optional(Type.String({ description: "Role spec — .pi/teammates/<role>.md or .claude/teammates/<role>.md." })),
195
+ model: Type.Optional(Type.String({ description: "Override: full spec (\"openai-codex/gpt-5.4\") or tier (\"xs\"/\"sm\"/\"md\"/\"lg\"/\"xl\", legacy \"cheap\"/\"mid\"/\"deep\")." })),
196
+ thinking: Type.Optional(ThinkingLevelSchema),
197
+ thinking_level: Type.Optional(ThinkingLevelSchema),
198
+ isolation: Type.Optional(IsolationSchema),
199
+ runtime: Type.Optional(RuntimeSchema),
200
+ run_in_background: Type.Optional(Type.Boolean({ description: "Return immediately; worker keeps running. Completion arrives as <task-notification>." })),
201
+ });
202
+
203
+ const DelegateTaskParams = Type.Object({
204
+ description: Type.String({ description: "Short (3–5 word) task label." }),
205
+ prompt: Type.String({ description: "Self-contained task brief. Workers don't see the coordinator's conversation." }),
206
+ name: Type.Optional(Type.String({ description: "Unique teammate name. Pass as `to` in send_message to continue." })),
207
+ team_name: Type.Optional(Type.String({ description: "Team id from team_create (optional grouping)." })),
208
+ subagent_type: Type.Optional(Type.String({ description: "Role spec — .pi/teammates/<role>.md or .claude/teammates/<role>.md." })),
209
+ model: Type.Optional(Type.String({ description: "Override: full spec (\"openai-codex/gpt-5.4\") or tier (\"xs\"/\"sm\"/\"md\"/\"lg\"/\"xl\", legacy \"cheap\"/\"mid\"/\"deep\")." })),
210
+ thinking: Type.Optional(ThinkingLevelSchema),
211
+ thinking_level: Type.Optional(ThinkingLevelSchema),
212
+ isolation: Type.Optional(IsolationSchema),
213
+ runtime: Type.Optional(RuntimeSchema),
214
+ count: Type.Optional(Type.Number()),
215
+ output: Type.Optional(Type.Union([Type.String(), Type.Boolean()])),
216
+ reads: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Boolean()])),
217
+ });
218
+
219
+ const DelegateChainParallelStepParams = Type.Object({
220
+ parallel: Type.Array(DelegateTaskParams, { minItems: 1, description: "Array of task objects, each with description + prompt." }),
221
+ concurrency: Type.Optional(Type.Number()),
222
+ failFast: Type.Optional(Type.Boolean()),
223
+ isolation: Type.Optional(IsolationSchema),
224
+ runtime: Type.Optional(RuntimeSchema),
225
+ });
226
+
227
+ const DelegateChainStepParams = Type.Union([
228
+ DelegateTaskParams,
229
+ DelegateChainParallelStepParams,
230
+ ]);
231
+
232
+ const DelegateParams = Type.Object({
233
+ task: Type.Optional(Type.String()),
234
+ tasks: Type.Optional(Type.Array(DelegateTaskParams)),
235
+ chain: Type.Optional(Type.Array(DelegateChainStepParams)),
236
+ concurrency: Type.Optional(Type.Number()),
237
+ isolation: Type.Optional(IsolationSchema),
238
+ runtime: Type.Optional(RuntimeSchema),
239
+ });
240
+
241
+ const SendMessageParams = Type.Object({
242
+ to: Type.String({ description: "Worker's task_id or name. Use \"*\" (swarm only) to broadcast to all active teammates." }),
243
+ message: Type.String(),
244
+ });
245
+
246
+ const TaskStopParams = Type.Object({
247
+ task_id: Type.String({ description: "Worker's task_id (from agent tool's result)." }),
248
+ });
249
+
250
+ const TaskOutputParams = Type.Object({
251
+ task_id: Type.String(),
252
+ });
253
+
254
+ const TeamCreateParams = Type.Object({
255
+ name: Type.String(),
256
+ default_isolation: Type.Optional(IsolationSchema),
257
+ worktree_base: Type.Optional(Type.String()),
258
+ });
259
+
260
+ const TeamDeleteParams = Type.Object({ team_id: Type.String() });
261
+
262
+ const TaskCreateParams = Type.Object({
263
+ subject: Type.String({ description: "Brief, actionable title in imperative form." }),
264
+ description: Type.String({ description: "What needs to be done." }),
265
+ activeForm: Type.Optional(Type.String({ description: "Present-continuous form shown when in_progress." })),
266
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
267
+ });
268
+
269
+ const TaskUpdateParams = Type.Object({
270
+ task_id: Type.String(),
271
+ status: Type.Optional(TaskStatusSchema),
272
+ owner: Type.Optional(Type.Union([Type.String(), Type.Null()])),
273
+ subject: Type.Optional(Type.String()),
274
+ description: Type.Optional(Type.String()),
275
+ activeForm: Type.Optional(Type.String()),
276
+ result: Type.Optional(Type.String()),
277
+ addBlocks: Type.Optional(Type.Array(Type.String())),
278
+ addBlockedBy: Type.Optional(Type.Array(Type.String())),
279
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
280
+ expected_version: Type.Optional(Type.Number({ description: "CAS guard." })),
281
+ });
282
+
283
+ const TaskGetParams = Type.Object({ task_id: Type.String() });
284
+
285
+ const TaskListParams = Type.Object({
286
+ status: Type.Optional(TaskStatusSchema),
287
+ owner: Type.Optional(Type.String()),
288
+ });
289
+
290
+ // --- activation ---
291
+
292
+ export function activate(pi: ExtensionAPI): void {
293
+ if (isSubprocess()) {
294
+ activateSubprocess(pi);
295
+ return;
296
+ }
297
+ activateParent(pi);
298
+ }
299
+
300
+ export default activate;
301
+
302
+ function activateParent(pi: ExtensionAPI): void {
303
+ pi.registerMessageRenderer("task-notification", renderTaskNotification);
304
+ registerAgentTools(pi);
305
+ registerDelegateTools(pi);
306
+ registerTeamTools(pi);
307
+ registerTaskTools(pi);
308
+ registerCommands(pi);
309
+ registerLifecycle(pi);
310
+ registerCoordinatorPromptHook(pi);
311
+ }
312
+
313
+ function activateSubprocess(pi: ExtensionAPI): void {
314
+ initSubprocess();
315
+ registerTaskTools(pi);
316
+ }
317
+
318
+ // --- agent tools (parent only) ---
319
+
320
+ function registerAgentTools(pi: ExtensionAPI): void {
321
+ pi.registerTool({
322
+ name: "agent",
323
+ label: "Spawn Worker",
324
+ description:
325
+ "Spawn a worker as an isolated pi subprocess. Returns the task_id immediately. The coordinator should end its turn after launching; a `<task-notification>` will arrive as a user-role message when the worker finishes. Use send_message to continue an existing worker with its loaded context. Parallel calls in one turn run concurrently.",
326
+ promptSnippet: "Spawn a worker to research, implement, or verify",
327
+ promptGuidelines: [
328
+ "Pass `name` to address the worker later via send_message.",
329
+ "`isolation: \"worktree\"` sandboxes edits in a git worktree.",
330
+ "Launch multiple workers in parallel when the work is independent.",
331
+ "After launching, briefly tell the user what you launched and end your turn — completion arrives as <task-notification>.",
332
+ "Never predict worker results — wait for the notification.",
333
+ ],
334
+ parameters: AgentParams,
335
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
336
+ const { agents } = getParentManagers();
337
+ const result = await agents.spawn({
338
+ description: params.description,
339
+ prompt: params.prompt,
340
+ name: params.name,
341
+ teamId: params.team_name,
342
+ subagentType: params.subagent_type,
343
+ model: params.model,
344
+ thinkingLevel: params.thinking ?? params.thinking_level,
345
+ isolation: params.isolation,
346
+ runtime: params.runtime,
347
+ background: params.run_in_background,
348
+ });
349
+ await refreshWidget(ctx);
350
+ return {
351
+ content: [{ type: "text", text: formatSpawnResult(result) }],
352
+ details: result,
353
+ };
354
+ },
355
+ });
356
+
357
+ pi.registerTool({
358
+ name: "send_message",
359
+ label: "Message Worker",
360
+ description:
361
+ "Continue an existing worker with full prior context (reuses pi --session). Pass the worker's task_id or name as `to`. Use `to: \"*\"` (swarm only) to broadcast.",
362
+ promptSnippet: "Continue a worker",
363
+ promptGuidelines: [
364
+ "Use send_message when you want the worker to remember what it already did.",
365
+ "Synthesize findings into a specific spec — never write \"based on your findings\".",
366
+ ],
367
+ parameters: SendMessageParams,
368
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
369
+ const { agents } = getParentManagers();
370
+ const result = await agents.sendMessage(params.to, params.message);
371
+ await refreshWidget(ctx);
372
+ return {
373
+ content: [{ type: "text", text: formatSpawnResult(result) }],
374
+ details: result,
375
+ };
376
+ },
377
+ });
378
+
379
+ pi.registerTool({
380
+ name: "task_stop",
381
+ label: "Stop Worker",
382
+ description: "Stop a running worker by task_id. Stopped workers can be continued with send_message.",
383
+ parameters: TaskStopParams,
384
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
385
+ const { agents } = getParentManagers();
386
+ await agents.stop(params.task_id);
387
+ await refreshWidget(ctx);
388
+ return {
389
+ content: [{ type: "text", text: `Stopped ${params.task_id}.` }],
390
+ details: { task_id: params.task_id },
391
+ };
392
+ },
393
+ });
394
+
395
+ pi.registerTool({
396
+ name: "task_output",
397
+ label: "Get Worker Output",
398
+ description:
399
+ "Read the current or last output of a worker by task_id. Useful when a worker reported partial progress and you want to inspect it without sending a new message.",
400
+ parameters: TaskOutputParams,
401
+ async execute(_toolCallId, params) {
402
+ const { agents } = getParentManagers();
403
+ const record = await agents.output(params.task_id);
404
+ if (!record) {
405
+ return {
406
+ content: [{ type: "text", text: `Unknown worker: ${params.task_id}` }],
407
+ details: undefined,
408
+ };
409
+ }
410
+ return {
411
+ content: [{ type: "text", text: formatTeammateStatus(record) }],
412
+ details: record,
413
+ };
414
+ },
415
+ });
416
+ }
417
+
418
+ function registerDelegateTools(pi: ExtensionAPI): void {
419
+ pi.registerTool({
420
+ name: "delegate",
421
+ label: "Delegate Group",
422
+ description:
423
+ "Run a foreground delegation group.\n\nTwo mutually exclusive modes:\n- tasks[] — bounded parallel fan-out. Each item MUST have `description` and `prompt` fields.\n- chain[] — sequential workflow steps. Each step MUST be an object with `description` and `prompt` fields. To fan out workers inside a chain step, add a `parallel` array: { description, prompt, parallel: [{ description, prompt }, ...] }.\n\nTemplate substitutions available in prompt strings: {task}, {previous}, {chain_dir}.",
424
+ parameters: DelegateParams,
425
+ async execute(_toolCallId, params) {
426
+ const { delegations } = getParentManagers();
427
+ const hasTasks = Array.isArray(params.tasks) && params.tasks.length > 0;
428
+ const hasChain = Array.isArray(params.chain) && params.chain.length > 0;
429
+ if (hasTasks === hasChain) {
430
+ throw new Error("delegate requires exactly one mode: either tasks[] or chain[]");
431
+ }
432
+
433
+ if (hasTasks) {
434
+ const result = await delegations.runParallel({
435
+ tasks: (params.tasks ?? []).map(mapDelegateTask),
436
+ concurrency: params.concurrency,
437
+ isolation: params.isolation,
438
+ runtime: params.runtime,
439
+ });
440
+ return {
441
+ content: [{ type: "text", text: formatDelegateResult(result) }],
442
+ details: result,
443
+ };
444
+ }
445
+
446
+ const chainSteps = (params.chain ?? []).map((step) => {
447
+ if ("parallel" in step) {
448
+ return {
449
+ parallel: step.parallel.map(mapDelegateTask),
450
+ concurrency: step.concurrency,
451
+ failFast: step.failFast,
452
+ isolation: step.isolation,
453
+ runtime: step.runtime,
454
+ };
455
+ }
456
+ return mapDelegateTask(step);
457
+ });
458
+ const usesTaskTemplate = JSON.stringify(chainSteps).includes("{task}");
459
+ if (usesTaskTemplate && !(params.task && params.task.trim())) {
460
+ throw new Error("delegate chain mode requires top-level task when {task} is used");
461
+ }
462
+ const result = await delegations.runChain({
463
+ task: params.task ?? "",
464
+ chain: chainSteps,
465
+ concurrency: params.concurrency,
466
+ isolation: params.isolation,
467
+ runtime: params.runtime,
468
+ });
469
+ return {
470
+ content: [{ type: "text", text: formatDelegateResult(result) }],
471
+ details: result,
472
+ };
473
+ },
474
+ });
475
+ }
476
+
477
+ function mapDelegateTask(task: {
478
+ description: string;
479
+ prompt: string;
480
+ name?: string;
481
+ team_name?: string;
482
+ subagent_type?: string;
483
+ model?: string;
484
+ thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
485
+ thinking_level?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
486
+ isolation?: "none" | "worktree";
487
+ runtime?: "subprocess" | "transient";
488
+ count?: number;
489
+ output?: string | boolean;
490
+ reads?: string[] | boolean;
491
+ }) {
492
+ return {
493
+ description: task.description,
494
+ prompt: task.prompt,
495
+ name: task.name,
496
+ teamId: task.team_name,
497
+ subagentType: task.subagent_type,
498
+ model: task.model,
499
+ thinkingLevel: task.thinking ?? task.thinking_level,
500
+ isolation: task.isolation,
501
+ runtime: task.runtime,
502
+ count: task.count,
503
+ output: task.output === true ? undefined : task.output,
504
+ reads: task.reads === true ? undefined : task.reads,
505
+ };
506
+ }
507
+
508
+ function registerTeamTools(pi: ExtensionAPI): void {
509
+ pi.registerTool({
510
+ name: "team_create",
511
+ label: "Create Team",
512
+ description: "Create a team namespace for grouping workers. Sets default isolation + worktree base for bulk spawns.",
513
+ parameters: TeamCreateParams,
514
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
515
+ const { teams } = getParentManagers();
516
+ const team = await teams.create({
517
+ name: params.name,
518
+ defaultIsolation: params.default_isolation,
519
+ worktreeBase: params.worktree_base,
520
+ });
521
+ await refreshWidget(ctx);
522
+ return {
523
+ content: [{ type: "text", text: `Team created: ${team.name} (${team.id}).` }],
524
+ details: team,
525
+ };
526
+ },
527
+ });
528
+
529
+ pi.registerTool({
530
+ name: "team_delete",
531
+ label: "Delete Team",
532
+ description: "Delete a team, stopping all its workers. Worktrees with changes are retained.",
533
+ parameters: TeamDeleteParams,
534
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
535
+ const { teams } = getParentManagers();
536
+ await teams.delete(params.team_id);
537
+ await refreshWidget(ctx);
538
+ return {
539
+ content: [{ type: "text", text: `Team deleted: ${params.team_id}.` }],
540
+ details: { team_id: params.team_id },
541
+ };
542
+ },
543
+ });
544
+ }
545
+
546
+ // --- task tools (parent + subprocess) ---
547
+
548
+ function registerTaskTools(pi: ExtensionAPI): void {
549
+ pi.registerTool({
550
+ name: "task_create",
551
+ label: "Create Task",
552
+ description:
553
+ "Create a structured task in the shared TODO list. Tasks are created with status 'pending' and no owner — the coordinator assigns owners via task_update.",
554
+ promptSnippet: "Track a multi-step task",
555
+ promptGuidelines: [
556
+ "Use for complex multi-step tasks (3+ steps) or when the user provides a list.",
557
+ "Mark a task in_progress BEFORE beginning work, completed when done.",
558
+ "Include enough detail in `description` for another agent to understand and complete the task.",
559
+ "Use task_update to set dependencies (addBlocks / addBlockedBy).",
560
+ ],
561
+ parameters: TaskCreateParams,
562
+ async execute(_toolCallId, params) {
563
+ const task = await getTaskManager().create({
564
+ subject: params.subject,
565
+ description: params.description,
566
+ activeForm: params.activeForm,
567
+ metadata: params.metadata,
568
+ });
569
+ return {
570
+ content: [{ type: "text", text: formatTaskLine(task) }],
571
+ details: { task: { id: task.id, subject: task.subject } },
572
+ };
573
+ },
574
+ });
575
+
576
+ pi.registerTool({
577
+ name: "task_update",
578
+ label: "Update Task",
579
+ description:
580
+ "Update a task's status, owner, fields, or dependencies. Pass `expected_version` (from task_get/task_list) to guard concurrent edits. Transition to 'completed' fires the TaskCompleted hook — non-zero exit reverts the task to 'failed'.",
581
+ promptSnippet: "Update a task",
582
+ promptGuidelines: [
583
+ "Mark the current task in_progress when you start; completed when done.",
584
+ "Assign workers by setting `owner` to their teammate name.",
585
+ "ONLY mark a task completed when fully done — tests passing, no partial work.",
586
+ ],
587
+ parameters: TaskUpdateParams,
588
+ async execute(_toolCallId, params) {
589
+ try {
590
+ const updated = await getTaskManager().update(params.task_id, {
591
+ status: params.status,
592
+ owner: params.owner,
593
+ subject: params.subject,
594
+ description: params.description,
595
+ activeForm: params.activeForm,
596
+ result: params.result,
597
+ addBlocks: params.addBlocks,
598
+ addBlockedBy: params.addBlockedBy,
599
+ metadata: params.metadata,
600
+ expectedVersion: params.expected_version,
601
+ });
602
+ return { content: [{ type: "text", text: formatTaskDetails(updated) }], details: updated };
603
+ } catch (err) {
604
+ if (err instanceof VersionConflictError) {
605
+ return {
606
+ content: [
607
+ {
608
+ type: "text",
609
+ text: `Version conflict: task advanced to version ${err.actual}. Re-fetch and retry.`,
610
+ },
611
+ ],
612
+ details: undefined,
613
+ };
614
+ }
615
+ throw err;
616
+ }
617
+ },
618
+ });
619
+
620
+ pi.registerTool({
621
+ name: "task_get",
622
+ label: "Get Task",
623
+ description: "Fetch a single task's full details by id.",
624
+ parameters: TaskGetParams,
625
+ async execute(_toolCallId, params) {
626
+ const task = await getTaskManager().get(params.task_id);
627
+ if (!task) {
628
+ return {
629
+ content: [{ type: "text", text: `Unknown task: ${params.task_id}` }],
630
+ details: undefined,
631
+ };
632
+ }
633
+ return { content: [{ type: "text", text: formatTaskDetails(task) }], details: task };
634
+ },
635
+ });
636
+
637
+ pi.registerTool({
638
+ name: "task_list",
639
+ label: "List Tasks",
640
+ description: "List all tasks in the shared TODO list. Filter by status or owner.",
641
+ parameters: TaskListParams,
642
+ async execute(_toolCallId, params) {
643
+ const list = await getTaskManager().list({ status: params.status, owner: params.owner });
644
+ return { content: [{ type: "text", text: formatTaskList(list) }], details: list };
645
+ },
646
+ });
647
+ }
648
+
649
+ // --- slash commands (parent only) ---
650
+
651
+ function registerCommands(pi: ExtensionAPI): void {
652
+ pi.registerCommand("teammate", {
653
+ description: "Manage workers: /teammate list | status <name> | stop <name>",
654
+ handler: async (args, ctx) => {
655
+ const { agents } = getParentManagers();
656
+ const parts = (args?.trim() ?? "").split(/\s+/).filter(Boolean);
657
+ const sub = parts[0]?.toLowerCase() ?? "list";
658
+ switch (sub) {
659
+ case "list":
660
+ ctx.ui.notify(formatTeammateList(await agents.list()), "info");
661
+ return;
662
+ case "status": {
663
+ const name = parts[1];
664
+ if (!name) return ctx.ui.notify("Usage: /teammate status <name>", "warning");
665
+ const record = await agents.get(name);
666
+ if (!record) return ctx.ui.notify(`Unknown worker: ${name}`, "error");
667
+ ctx.ui.notify(formatTeammateStatus(record), "info");
668
+ return;
669
+ }
670
+ case "stop": {
671
+ const name = parts[1];
672
+ if (!name) return ctx.ui.notify("Usage: /teammate stop <name>", "warning");
673
+ await agents.stop(name);
674
+ await refreshWidget(ctx);
675
+ ctx.ui.notify(`Stopped ${name}.`, "info");
676
+ return;
677
+ }
678
+ default:
679
+ ctx.ui.notify(`Unknown subcommand: ${sub}`, "warning");
680
+ }
681
+ },
682
+ });
683
+
684
+ pi.registerCommand("team", {
685
+ description: "Manage teams: /team list | create <name> | delete <id>",
686
+ handler: async (args, ctx) => {
687
+ const { teams, agents } = getParentManagers();
688
+ const parts = (args?.trim() ?? "").split(/\s+/).filter(Boolean);
689
+ const sub = parts[0]?.toLowerCase() ?? "list";
690
+ switch (sub) {
691
+ case "list": {
692
+ const [teamList, teammates] = await Promise.all([teams.list(), agents.list()]);
693
+ ctx.ui.notify(formatTeamDashboard(teamList, teammates), "info");
694
+ return;
695
+ }
696
+ case "create": {
697
+ const name = parts.slice(1).join(" ");
698
+ if (!name) return ctx.ui.notify("Usage: /team create <name>", "warning");
699
+ const team = await teams.create({ name });
700
+ ctx.ui.notify(`Created team ${team.name} (${team.id}).`, "info");
701
+ return;
702
+ }
703
+ case "delete": {
704
+ const id = parts[1];
705
+ if (!id) return ctx.ui.notify("Usage: /team delete <id>", "warning");
706
+ await teams.delete(id);
707
+ await refreshWidget(ctx);
708
+ ctx.ui.notify(`Deleted team ${id}.`, "info");
709
+ return;
710
+ }
711
+ default:
712
+ ctx.ui.notify(`Unknown subcommand: ${sub}`, "warning");
713
+ }
714
+ },
715
+ });
716
+
717
+ pi.registerCommand("tasks", {
718
+ description: "Show the shared task list: /tasks [list|show <id>|clear]",
719
+ handler: async (args, ctx) => {
720
+ const { tasks } = getParentManagers();
721
+ const parts = (args?.trim() ?? "").split(/\s+/).filter(Boolean);
722
+ const sub = (parts[0] ?? "list").toLowerCase();
723
+ switch (sub) {
724
+ case "list": {
725
+ ctx.ui.notify(formatTaskList(await tasks.list()), "info");
726
+ return;
727
+ }
728
+ case "show": {
729
+ const id = parts[1];
730
+ if (!id) return ctx.ui.notify("Usage: /tasks show <id>", "warning");
731
+ const task = await tasks.get(id);
732
+ if (!task) return ctx.ui.notify(`Unknown task: ${id}`, "error");
733
+ ctx.ui.notify(formatTaskDetails(task), "info");
734
+ return;
735
+ }
736
+ case "clear":
737
+ await tasks.clear();
738
+ ctx.ui.notify("Cleared all tasks for this session.", "info");
739
+ return;
740
+ default:
741
+ ctx.ui.notify(`Unknown subcommand: ${sub}`, "warning");
742
+ }
743
+ },
744
+ });
745
+
746
+ try {
747
+ pi.registerShortcut("ctrl+shift+t", {
748
+ description: "Show the shared task list (team-mode)",
749
+ handler: async (ctx: ExtensionContext) => {
750
+ const { tasks } = getParentManagers();
751
+ ctx.ui.notify(formatTaskList(await tasks.list()), "info");
752
+ },
753
+ });
754
+ } catch {
755
+ /* non-fatal — the /tasks command still works */
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Inject the coordinator system prompt into every turn when the parent
761
+ * session is running in coordinator mode. Uses the `before_agent_start`
762
+ * hook so the addition survives session reloads and is deterministic.
763
+ */
764
+ function registerCoordinatorPromptHook(pi: ExtensionAPI): void {
765
+ pi.on("before_agent_start", async (event) => {
766
+ if (!isCoordinatorMode()) return undefined;
767
+ const addition = getCoordinatorSystemPrompt();
768
+ const combined = event.systemPrompt
769
+ ? `${event.systemPrompt}\n\n${addition}`
770
+ : addition;
771
+ return { systemPrompt: combined };
772
+ });
773
+ }
774
+
775
+ function registerLifecycle(pi: ExtensionAPI): void {
776
+ pi.on("session_start", async (event, ctx) => {
777
+ disposeWidget?.();
778
+ disposeWidget = undefined;
779
+ if (event.reason !== "startup" && parentManagers) {
780
+ await parentManagers.agents.cleanup();
781
+ }
782
+ const managers = await initParent(pi, ctx);
783
+ disposeWidget = startTeamMateWidget(ctx, managers.agents);
784
+ });
785
+
786
+ pi.on("session_shutdown", async () => {
787
+ disposeWidget?.();
788
+ disposeWidget = undefined;
789
+ if (parentManagers) await parentManagers.agents.cleanup();
790
+ });
791
+ }
792
+
793
+ // --- helpers ---
794
+
795
+ function formatSpawnResult(result: TeammateRunResult): string {
796
+ const modelStr =
797
+ result.provider && result.model
798
+ ? `${result.provider}/${result.model}`
799
+ : result.model ?? "(pi default)";
800
+ const runtime = result.runtime ?? "subprocess";
801
+ const lines = [
802
+ `task_id: ${result.teammateId}`,
803
+ `Worker: ${result.name} (status=${result.status}, exit=${result.exitCode ?? "n/a"}, runtime=${runtime})`,
804
+ `Model: ${modelStr}${result.thinkingLevel ? ` (thinking=${result.thinkingLevel})` : ""}${result.modelRationale ? ` — ${result.modelRationale}` : ""}`,
805
+ ];
806
+ if (result.worktree) {
807
+ lines.push(`Worktree retained: ${result.worktree.path} (branch ${result.worktree.branch})`);
808
+ }
809
+ if (result.background) {
810
+ lines.push(result.result);
811
+ } else if (result.result) {
812
+ lines.push("", result.result);
813
+ }
814
+ return lines.join("\n");
815
+ }
816
+
817
+ function formatDelegateResult(result: DelegationResult): string {
818
+ if (result.mode === "parallel") return result.output;
819
+ return [
820
+ `Chain completed: ${result.steps} step(s)`,
821
+ `chain_dir: ${result.chainDir ?? "(none)"}`,
822
+ "",
823
+ result.output,
824
+ ].join("\n");
825
+ }