tau-coding-agent 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import { Type } from "typebox";
10
- import { complete } from "@earendil-works/pi-ai/compat";
11
10
  import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
12
11
  import {
13
12
  compact,
@@ -25,7 +24,7 @@ const LOOP_PRESETS = [
25
24
  ] as const;
26
25
 
27
26
  const LOOP_STATE_ENTRY = "loop-state";
28
- const OPENAI_FAST_MODEL_ID = "gpt-5.3-codex-spark";
27
+ const OPENAI_FAST_MODEL_ID = "gpt-5.6-luna";
29
28
  const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
30
29
 
31
30
  const SUMMARY_SYSTEM_PROMPT = `You summarize loop breakout conditions for a status widget.
@@ -47,30 +46,12 @@ type LoopStateData = {
47
46
  loopCount?: number;
48
47
  };
49
48
 
50
- type PromptStatus = "completed" | "error";
51
49
  type ModelFamily = "openai" | "anthropic";
52
50
 
53
- type ModelRequestAuth = {
51
+ type ModelSelection = {
54
52
  model: Model<Api>;
55
- apiKey?: string;
56
- headers?: Record<string, string>;
57
- env?: Record<string, string>;
58
53
  };
59
54
 
60
- async function withPromptSignal<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T> {
61
- pi.events.emit("ui:prompt_start", { source: "loop" });
62
-
63
- let status: PromptStatus = "completed";
64
- try {
65
- return await run();
66
- } catch (error) {
67
- status = "error";
68
- throw error;
69
- } finally {
70
- pi.events.emit("ui:prompt_end", { source: "loop", status });
71
- }
72
- }
73
-
74
55
  function detectModelFamily(provider: string): ModelFamily | null {
75
56
  const normalizedProvider = provider.toLowerCase();
76
57
  if (normalizedProvider.includes("openai")) return "openai";
@@ -121,7 +102,11 @@ function getConditionText(mode: LoopMode, condition?: string): string {
121
102
  }
122
103
  }
123
104
 
124
- async function selectSummaryModel(ctx: ExtensionContext): Promise<ModelRequestAuth | null> {
105
+ async function selectSummaryModel(
106
+ ctx: ExtensionContext,
107
+ signal: AbortSignal,
108
+ ): Promise<ModelSelection | null> {
109
+ signal.throwIfAborted();
125
110
  if (!ctx.model) return null;
126
111
 
127
112
  const family = detectModelFamily(ctx.model.provider);
@@ -137,30 +122,24 @@ async function selectSummaryModel(ctx: ExtensionContext): Promise<ModelRequestAu
137
122
  if (!candidate) continue;
138
123
 
139
124
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(candidate);
140
- if (auth.ok) {
141
- return {
142
- model: candidate,
143
- apiKey: auth.apiKey,
144
- headers: auth.headers,
145
- env: auth.env,
146
- };
147
- }
125
+ signal.throwIfAborted();
126
+ if (auth.ok) return { model: candidate };
148
127
  }
149
128
  }
150
129
 
151
130
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
152
- return auth.ok
153
- ? { model: ctx.model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env }
154
- : null;
131
+ signal.throwIfAborted();
132
+ return auth.ok ? { model: ctx.model } : null;
155
133
  }
156
134
 
157
135
  async function summarizeBreakoutCondition(
158
136
  ctx: ExtensionContext,
159
137
  mode: LoopMode,
160
- condition?: string,
138
+ condition: string | undefined,
139
+ signal: AbortSignal,
161
140
  ): Promise<string> {
162
141
  const fallback = summarizeCondition(mode, condition);
163
- const selection = await selectSummaryModel(ctx);
142
+ const selection = await selectSummaryModel(ctx, signal);
164
143
  if (!selection) return fallback;
165
144
 
166
145
  const conditionText = getConditionText(mode, condition);
@@ -170,10 +149,13 @@ async function summarizeBreakoutCondition(
170
149
  timestamp: Date.now(),
171
150
  };
172
151
 
173
- const response = await complete(
152
+ const response = await ctx.modelRegistry.complete(
174
153
  selection.model,
175
- { systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
176
- { apiKey: selection.apiKey, headers: selection.headers, env: selection.env },
154
+ {
155
+ systemPrompt: SUMMARY_SYSTEM_PROMPT,
156
+ messages: [userMessage],
157
+ },
158
+ { signal },
177
159
  );
178
160
 
179
161
  if (response.stopReason === "aborted" || response.stopReason === "error") {
@@ -209,8 +191,8 @@ function updateStatus(ctx: ExtensionContext, state: LoopStateData): void {
209
191
  ctx.ui.setWidget("loop", [ctx.ui.theme.fg("accent", text)]);
210
192
  }
211
193
 
212
- async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
213
- const entries = ctx.sessionManager.getEntries();
194
+ function loadState(ctx: ExtensionContext): LoopStateData {
195
+ const entries = ctx.sessionManager.getBranch();
214
196
  for (let i = entries.length - 1; i >= 0; i--) {
215
197
  const entry = entries[i] as { type: string; customType?: string; data?: LoopStateData };
216
198
  if (entry.type === "custom" && entry.customType === LOOP_STATE_ENTRY && entry.data) {
@@ -222,41 +204,74 @@ async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
222
204
 
223
205
  export default function loopExtension(pi: ExtensionAPI): void {
224
206
  let loopState: LoopStateData = { active: false };
225
- let lastAgentEndMessages: Array<{ role?: string; stopReason?: string }> = [];
207
+ let loopGeneration = 0;
208
+ let lastAssistantStopReason: string | undefined;
226
209
  let pendingLoopPrompt: ReturnType<typeof setImmediate> | undefined;
210
+ let pendingSummary: { controller: AbortController; done: Promise<void> } | undefined;
227
211
 
228
212
  function persistState(state: LoopStateData): void {
229
213
  pi.appendEntry(LOOP_STATE_ENTRY, state);
230
214
  }
231
215
 
232
- function setLoopState(state: LoopStateData, ctx: ExtensionContext): void {
216
+ async function setLoopState(state: LoopStateData, ctx: ExtensionContext): Promise<boolean> {
217
+ const generation = ++loopGeneration;
218
+ cancelScheduledLoopPrompt();
233
219
  loopState = state;
234
- persistState(state);
235
- updateStatus(ctx, state);
220
+ const cancelled = cancelSummary();
221
+ try {
222
+ persistState(state);
223
+ updateStatus(ctx, state);
224
+ } finally {
225
+ await cancelled;
226
+ }
227
+ return generation === loopGeneration;
236
228
  }
237
229
 
238
- function clearLoopState(ctx: ExtensionContext): void {
239
- const cleared: LoopStateData = { active: false };
240
- loopState = cleared;
241
- persistState(cleared);
242
- updateStatus(ctx, cleared);
230
+ async function clearLoopState(ctx: ExtensionContext): Promise<void> {
231
+ await setLoopState({ active: false }, ctx);
243
232
  }
244
233
 
245
- function breakLoop(ctx: ExtensionContext): void {
246
- clearLoopState(ctx);
234
+ async function breakLoop(ctx: ExtensionContext): Promise<void> {
235
+ await clearLoopState(ctx);
247
236
  ctx.ui.notify("Loop ended", "info");
248
237
  }
249
238
 
250
- function wasLastAssistantAborted(
251
- messages: Array<{ role?: string; stopReason?: string }>,
252
- ): boolean {
253
- for (let i = messages.length - 1; i >= 0; i--) {
254
- const message = messages[i];
255
- if (message?.role === "assistant") {
256
- return message.stopReason === "aborted";
239
+ async function cancelSummary(): Promise<void> {
240
+ const summary = pendingSummary;
241
+ if (!summary) return;
242
+ summary.controller.abort();
243
+ await summary.done;
244
+ }
245
+
246
+ function startSummary(ctx: ExtensionContext): void {
247
+ if (!loopState.active || !loopState.mode || loopState.summary) return;
248
+ const { mode, condition } = loopState;
249
+ const generation = loopGeneration;
250
+ // A loop can survive multiple turns and declined abort confirmations.
251
+ const controller = new AbortController();
252
+ const done = (async () => {
253
+ let summary: string;
254
+ try {
255
+ summary = await summarizeBreakoutCondition(ctx, mode, condition, controller.signal);
256
+ } catch (error) {
257
+ if (controller.signal.aborted || generation !== loopGeneration) return;
258
+ summary = summarizeCondition(mode, condition);
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ ctx.ui.notify(`Loop summary failed: ${message}`, "warning");
257
261
  }
258
- }
259
- return false;
262
+ if (controller.signal.aborted || generation !== loopGeneration) return;
263
+ loopState = { ...loopState, summary };
264
+ persistState(loopState);
265
+ updateStatus(ctx, loopState);
266
+ })().finally(() => {
267
+ if (pendingSummary?.controller === controller) pendingSummary = undefined;
268
+ });
269
+ pendingSummary = { controller, done };
270
+ // Publication failures must also be observed when no lifecycle handler is joining the task.
271
+ void done.catch((error: unknown) => {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ ctx.ui.notify(`Loop summary failed: ${message}`, "warning");
274
+ });
260
275
  }
261
276
 
262
277
  function triggerLoopPrompt(ctx: ExtensionContext): void {
@@ -306,41 +321,39 @@ export default function loopExtension(pi: ExtensionAPI): void {
306
321
  description: preset.description,
307
322
  }));
308
323
 
309
- const selection = await withPromptSignal(pi, () =>
310
- ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
311
- const container = new Container();
312
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
313
- container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));
314
-
315
- const selectList = new SelectList(items, Math.min(items.length, 10), {
316
- selectedPrefix: (text) => theme.fg("accent", text),
317
- selectedText: (text) => theme.fg("accent", text),
318
- description: (text) => theme.fg("muted", text),
319
- scrollInfo: (text) => theme.fg("dim", text),
320
- noMatch: (text) => theme.fg("warning", text),
321
- });
322
-
323
- selectList.onSelect = (item) => done(item.value);
324
- selectList.onCancel = () => done(null);
325
-
326
- container.addChild(selectList);
327
- container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
328
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
329
-
330
- return {
331
- render(width: number) {
332
- return container.render(width);
333
- },
334
- invalidate() {
335
- container.invalidate();
336
- },
337
- handleInput(data: string) {
338
- selectList.handleInput(data);
339
- tui.requestRender();
340
- },
341
- };
342
- }),
343
- );
324
+ const selection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
325
+ const container = new Container();
326
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
327
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));
328
+
329
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
330
+ selectedPrefix: (text) => theme.fg("accent", text),
331
+ selectedText: (text) => theme.fg("accent", text),
332
+ description: (text) => theme.fg("muted", text),
333
+ scrollInfo: (text) => theme.fg("dim", text),
334
+ noMatch: (text) => theme.fg("warning", text),
335
+ });
336
+
337
+ selectList.onSelect = (item) => done(item.value);
338
+ selectList.onCancel = () => done(null);
339
+
340
+ container.addChild(selectList);
341
+ container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
342
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
343
+
344
+ return {
345
+ render(width: number) {
346
+ return container.render(width);
347
+ },
348
+ invalidate() {
349
+ container.invalidate();
350
+ },
351
+ handleInput(data: string) {
352
+ selectList.handleInput(data);
353
+ tui.requestRender();
354
+ },
355
+ };
356
+ });
344
357
 
345
358
  if (!selection) return null;
346
359
 
@@ -350,9 +363,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
350
363
  case "self":
351
364
  return { active: true, mode: "self", prompt: buildPrompt("self") };
352
365
  case "custom": {
353
- const condition = await withPromptSignal(pi, () =>
354
- ctx.ui.editor("Enter loop breakout condition:", ""),
355
- );
366
+ const condition = await ctx.ui.editor("Enter loop breakout condition:", "");
356
367
  if (!condition?.trim()) return null;
357
368
  return {
358
369
  active: true,
@@ -428,7 +439,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
428
439
  };
429
440
  }
430
441
 
431
- clearLoopState(ctx);
442
+ await clearLoopState(ctx);
432
443
 
433
444
  return {
434
445
  content: [{ type: "text", text: "Loop ended." }],
@@ -458,9 +469,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
458
469
 
459
470
  if (loopState.active) {
460
471
  const confirm = ctx.hasUI
461
- ? await withPromptSignal(pi, () =>
462
- ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?"),
463
- )
472
+ ? await ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?")
464
473
  : true;
465
474
  if (!confirm) {
466
475
  ctx.ui.notify("Loop unchanged", "info");
@@ -469,36 +478,37 @@ export default function loopExtension(pi: ExtensionAPI): void {
469
478
  }
470
479
 
471
480
  const summarizedState: LoopStateData = { ...nextState, summary: undefined, loopCount: 0 };
472
- setLoopState(summarizedState, ctx);
481
+ if (!(await setLoopState(summarizedState, ctx))) return;
473
482
  ctx.ui.notify("Loop active", "info");
474
483
  triggerLoopPrompt(ctx);
475
484
 
476
- const mode = nextState.mode!;
477
- const condition = nextState.condition;
478
- void (async () => {
479
- const summary = await summarizeBreakoutCondition(ctx, mode, condition);
480
- if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition)
481
- return;
482
- loopState = { ...loopState, summary };
483
- persistState(loopState);
484
- updateStatus(ctx, loopState);
485
- })();
485
+ startSummary(ctx);
486
486
  },
487
487
  });
488
488
 
489
- pi.on("agent_end", async (event) => {
490
- lastAgentEndMessages = event.messages;
489
+ pi.on("agent_end", (event) => {
490
+ lastAssistantStopReason = event.messages.findLast(
491
+ (message) => message.role === "assistant",
492
+ )?.stopReason;
491
493
  });
492
494
 
493
495
  pi.on("agent_settled", async (_event, ctx) => {
494
496
  if (!loopState.active) return;
495
497
 
496
- if (ctx.hasUI && wasLastAssistantAborted(lastAgentEndMessages)) {
497
- const confirm = await withPromptSignal(pi, () =>
498
- ctx.ui.confirm("Break active loop?", "Operation aborted. Break out of the loop?"),
499
- );
498
+ if (lastAssistantStopReason === "error") {
499
+ await clearLoopState(ctx);
500
+ ctx.ui.notify("Loop stopped after an agent error", "error");
501
+ return;
502
+ }
503
+
504
+ if (lastAssistantStopReason === "aborted") {
505
+ const generation = loopGeneration;
506
+ const confirm =
507
+ !ctx.hasUI ||
508
+ (await ctx.ui.confirm("Break active loop?", "Operation aborted. Break out of the loop?"));
509
+ if (generation !== loopGeneration) return;
500
510
  if (confirm) {
501
- breakLoop(ctx);
511
+ await breakLoop(ctx);
502
512
  return;
503
513
  }
504
514
  }
@@ -508,9 +518,6 @@ export default function loopExtension(pi: ExtensionAPI): void {
508
518
 
509
519
  pi.on("session_before_compact", async (event, ctx) => {
510
520
  if (!loopState.active || !loopState.mode || !ctx.model) return;
511
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
512
- if (!auth.ok) return;
513
-
514
521
  const instructionParts = [
515
522
  event.customInstructions,
516
523
  getCompactionInstructions(loopState.mode, loopState.condition),
@@ -522,17 +529,16 @@ export default function loopExtension(pi: ExtensionAPI): void {
522
529
  const compaction = await compact(
523
530
  event.preparation,
524
531
  ctx.model,
525
- auth.apiKey,
526
- auth.headers,
532
+ undefined,
533
+ undefined,
527
534
  instructionParts,
528
535
  event.signal,
529
536
  ctx.thinkingLevel,
530
- undefined, // Use Pi's default summarization stream.
531
- auth.env,
537
+ ctx.modelRegistry.streamSimple.bind(ctx.modelRegistry),
532
538
  );
533
539
  return { compaction };
534
540
  } catch (error) {
535
- if (ctx.hasUI) {
541
+ if (ctx.hasUI && !event.signal.aborted) {
536
542
  const message = error instanceof Error ? error.message : String(error);
537
543
  ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
538
544
  }
@@ -541,28 +547,26 @@ export default function loopExtension(pi: ExtensionAPI): void {
541
547
  });
542
548
 
543
549
  async function restoreLoopState(ctx: ExtensionContext): Promise<void> {
544
- loopState = await loadState(ctx);
545
- updateStatus(ctx, loopState);
546
-
547
- if (loopState.active && loopState.mode && !loopState.summary) {
548
- const mode = loopState.mode;
549
- const condition = loopState.condition;
550
- void (async () => {
551
- const summary = await summarizeBreakoutCondition(ctx, mode, condition);
552
- if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition)
553
- return;
554
- loopState = { ...loopState, summary };
555
- persistState(loopState);
556
- updateStatus(ctx, loopState);
557
- })();
550
+ cancelScheduledLoopPrompt();
551
+ const generation = ++loopGeneration;
552
+ lastAssistantStopReason = undefined;
553
+ const cancelled = cancelSummary();
554
+ try {
555
+ loopState = loadState(ctx);
556
+ updateStatus(ctx, loopState);
557
+ } finally {
558
+ await cancelled;
558
559
  }
560
+ if (generation === loopGeneration) startSummary(ctx);
559
561
  }
560
562
 
561
- pi.on("session_start", async (_event, ctx) => {
562
- await restoreLoopState(ctx);
563
- });
563
+ pi.on("session_start", (_event, ctx) => restoreLoopState(ctx));
564
+
565
+ pi.on("session_tree", (_event, ctx) => restoreLoopState(ctx));
564
566
 
565
567
  pi.on("session_shutdown", async () => {
568
+ loopGeneration += 1;
566
569
  cancelScheduledLoopPrompt();
570
+ await cancelSummary();
567
571
  });
568
572
  }