@ghyper9023/pi-dev-workflow 0.4.3 → 0.5.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 (61) hide show
  1. package/.doc/AGENT-FRONTMATTER-REFERENCE.md +198 -0
  2. package/.version/RELEASE-v0.5.0.md +82 -0
  3. package/README.md +2 -0
  4. package/agents/git-agent.md +7 -0
  5. package/agents/grill/dev-doc-grill-agent.md +7 -0
  6. package/agents/grill/dev-fix-grill-agent.md +7 -0
  7. package/agents/grill/dev-grill-agent.md +7 -0
  8. package/agents/grill/dev-perf-grill-agent.md +7 -0
  9. package/agents/grill/dev-prd-agent.md +7 -0
  10. package/agents/grill/dev-refactor-grill-agent.md +7 -0
  11. package/agents/grill/dev-test-grill-agent.md +7 -0
  12. package/agents/review-agent.md +7 -0
  13. package/agents/workflow/docWriter-agent.md +13 -2
  14. package/agents/workflow/planner-agent.md +16 -3
  15. package/agents/workflow/reviewer-agent.md +21 -4
  16. package/agents/workflow/trimmer-agent.md +12 -1
  17. package/agents/workflow/worker-agent.md +17 -2
  18. package/extensions/sub-agents.ts +133 -16
  19. package/extensions/ui-helpers.ts +20 -20
  20. package/extensions/workflow-engine.ts +323 -16
  21. package/package.json +1 -1
  22. package/tests/test-workflow-engine-bugs.mjs +317 -0
  23. package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +0 -14
  24. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +0 -58
  25. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +0 -13
  26. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +0 -13
  27. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +0 -13
  28. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +0 -13
  29. package/.pi-dev-output/pi-grill/answers/answer-mpfttme1-20260522-0230.md +0 -13
  30. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +0 -94
  31. package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +0 -150
  32. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +0 -215
  33. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +0 -240
  34. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +0 -253
  35. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +0 -137
  36. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +0 -258
  37. package/.pi-dev-output/pi-plans/20260522-113000-grill-left-arrow-fix.md +0 -274
  38. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +0 -196
  39. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +0 -91
  40. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +0 -191
  41. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +0 -189
  42. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +0 -241
  43. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +0 -270
  44. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +0 -214
  45. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +0 -201
  46. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +0 -422
  47. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +0 -212
  48. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +0 -377
  49. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +0 -296
  50. package/.pi-dev-output/pi-review/md/review-20260522-105000.md +0 -166
  51. package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +0 -108
  52. package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +0 -402
  53. package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +0 -447
  54. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +0 -708
  55. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +0 -365
  56. package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +0 -395
  57. package/.pi-dev-output/pi-workflow/checkpoint-20260522-113000-grill-left-arrow-fix.json +0 -473
  58. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +0 -30
  59. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +0 -49
  60. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +0 -59
  61. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +0 -76
@@ -111,6 +111,15 @@ function osHomedir(): string {
111
111
  }
112
112
  }
113
113
 
114
+ /** Parse boolean-like frontmatter values: true/yes/1, false/no/0, or undefined. */
115
+ function parseBool(val: string | undefined): boolean | undefined {
116
+ if (val === undefined) return undefined;
117
+ const lc = val.toLowerCase().trim();
118
+ if (lc === "true" || lc === "yes" || lc === "1") return true;
119
+ if (lc === "false" || lc === "no" || lc === "0") return false;
120
+ return undefined;
121
+ }
122
+
114
123
  // ── Process lifecycle management ─────────────────────────────
115
124
  //
116
125
  // Fix #1: Prevent orphan sub-agents when pi exits or crashes.
@@ -172,6 +181,22 @@ export interface AgentDef {
172
181
  systemPrompt: string;
173
182
  /** Custom timeout in ms for this agent type. */
174
183
  timeoutMs?: number;
184
+
185
+ // ── Subprocess CLI args (from frontmatter) ──
186
+ /** Thinking level: "off" | "low" | "medium" | "high" | "xhigh" */
187
+ thinkingLevel?: string;
188
+ /** Persist session (default: false = --no-session) */
189
+ session?: boolean;
190
+ /** Session directory override */
191
+ sessionDir?: string;
192
+ /** Skip context files AGENTS.md/CLAUDE.md (default: true = -nc) */
193
+ noContext?: boolean;
194
+ /** Disable extension loading (default: true = -ne) */
195
+ noExtensions?: boolean;
196
+ /** Output mode: "json" | "text" (default: "json") */
197
+ mode?: string;
198
+ /** Extra raw CLI args to append */
199
+ extraArgs?: string[];
175
200
  }
176
201
 
177
202
  export interface SubagentResult {
@@ -182,6 +207,33 @@ export interface SubagentResult {
182
207
  durationMs: number;
183
208
  }
184
209
 
210
+ /**
211
+ * Caller-provided overrides for subprocess CLI args.
212
+ * Each field, when set, takes precedence over the AgentDef (frontmatter) value.
213
+ */
214
+ export interface SubagentArgs {
215
+ /** Override thinking level */
216
+ thinkingLevel?: string;
217
+ /** Override session persistence */
218
+ session?: boolean;
219
+ /** Override session directory */
220
+ sessionDir?: string;
221
+ /** Override -nc (no context files) */
222
+ noContext?: boolean;
223
+ /** Override -ne (no extensions) */
224
+ noExtensions?: boolean;
225
+ /** Override --mode */
226
+ mode?: string;
227
+ /** Override --tools whitelist */
228
+ tools?: string[];
229
+ /** Extra --append-system-prompt (appended after agent's own) */
230
+ appendSystemPrompt?: string;
231
+ /** Extra raw CLI args (appended last, highest priority) */
232
+ extraArgs?: string[];
233
+ /** Workflow UUID to include in session name for cross-workflow traceability */
234
+ workflowId?: string;
235
+ }
236
+
185
237
  function inferTimeout(name: string): number {
186
238
  const lc = name.toLowerCase();
187
239
  if (lc.includes("review") || lc.includes("审查")) return REVIEW_TIMEOUT_MS;
@@ -201,7 +253,7 @@ function loadAgent(filePath: string): AgentDef | null {
201
253
 
202
254
  const fields: Record<string, string> = {};
203
255
  for (const line of frontRaw.split("\n")) {
204
- const m = line.match(/^(\w+):\s*(.*)$/);
256
+ const m = line.match(/^([\w-]+):\s*(.*)$/);
205
257
  if (m) fields[m[1]] = m[2];
206
258
  }
207
259
 
@@ -214,6 +266,14 @@ function loadAgent(filePath: string): AgentDef | null {
214
266
  tools: tools && tools.length > 0 ? tools : undefined,
215
267
  systemPrompt: body,
216
268
  timeoutMs: inferTimeout(fields.name),
269
+ // ── Subprocess args from frontmatter ──
270
+ thinkingLevel: fields.thinking || undefined,
271
+ session: parseBool(fields.session),
272
+ sessionDir: fields["session-dir"] || undefined,
273
+ noContext: parseBool(fields["no-context"]),
274
+ noExtensions: parseBool(fields["no-extensions"]),
275
+ mode: fields.mode || undefined,
276
+ extraArgs: fields["extra-args"] ? fields["extra-args"].split(/\s+/).filter(Boolean) : undefined,
217
277
  };
218
278
  } catch {
219
279
  return null;
@@ -270,8 +330,9 @@ export function discoverAgents(): AgentDef[] {
270
330
  // Fix #2: Simplified settle logic - no listener manipulation races.
271
331
  // Buffer is capped at MAX_BUFFER_BYTES to prevent OOM on runaway output.
272
332
  // Fix #3: onProgress callback streams sub-agent output in real-time.
273
- // Fix #4: -nc (no context files), -ne (no extensions) to reduce startup overhead.
274
- // Auto-load append.system.md. Thinking=off for faster responses.
333
+ // Fix #4: Subprocess CLI args are resolved from caller > AgentDef frontmatter > defaults.
334
+ // Defaults: -nc, -ne, --no-session, --mode json, --thinking off.
335
+ // Auto-load append.system.md for project-level instructions.
275
336
 
276
337
  function getPiInvocation(args: string[]): { command: string; args: string[] } {
277
338
  const currentScript = process.argv[1];
@@ -294,33 +355,65 @@ export async function spawnSubagent(
294
355
  signal?: AbortSignal,
295
356
  timeoutMs?: number,
296
357
  onProgress?: (msg: string) => void,
358
+ argsOverride?: SubagentArgs,
297
359
  ): Promise<SubagentResult> {
298
360
  const effectiveTimeout = timeoutMs ?? agent.timeoutMs ?? DEFAULT_TIMEOUT_MS;
299
361
  const systemPrompt = agent.systemPrompt.trim();
300
362
 
363
+ // ── Resolve arg overrides (caller > AgentDef frontmatter > hardcoded default) ──
364
+ const override = (argsOverride as SubagentArgs) ?? {};
365
+
366
+ const finalThinking = override.thinkingLevel ?? agent.thinkingLevel ?? "off";
367
+ const finalSession = override.session ?? agent.session ?? false;
368
+ const finalSessionDir = override.sessionDir ?? agent.sessionDir;
369
+ const finalNoContext = override.noContext ?? agent.noContext ?? true;
370
+ const finalNoExtensions = override.noExtensions ?? agent.noExtensions ?? true;
371
+ const finalMode = override.mode ?? agent.mode ?? "json";
372
+ const finalTools = override.tools ?? agent.tools;
373
+ const finalExtraArgs = override.extraArgs ?? agent.extraArgs;
374
+
301
375
  // Build pi arguments for the subagent
302
- const args: string[] = [
303
- "-p", // non-interactive
304
- "--no-session", // ephemeral
305
- "-nc", // no context files (AGENTS.md / CLAUDE.md)
306
- "-ne", // no extensions (less startup overhead)
307
- "--mode", "json", // structured output
308
- ];
376
+ const args: string[] = ["-p"]; // non-interactive
377
+
378
+ if (!finalSession) {
379
+ args.push("--no-session");
380
+ } else {
381
+ // Session enabled — auto-name with timestamp + agent name
382
+ // Use full file path with .jsonl extension (pi --session <path> creates/opens file)
383
+ const sessionDir = finalSessionDir || ".pi-dev-output/pi-subagent-sessions";
384
+ const safeAgentName = agent.name.replace(/[^a-zA-Z0-9_-]/g, "_");
385
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
386
+ const wfSuffix = override.workflowId ? `_${override.workflowId}` : "";
387
+ const sessionName = `${ts}_${safeAgentName}${wfSuffix}`;
388
+ // Ensure session directory exists before spawning subagent
389
+ try {
390
+ fs.mkdirSync(sessionDir, { recursive: true });
391
+ } catch { /* ignore if already exists */ }
392
+ const sessionPath = path.join(sessionDir, `${sessionName}.jsonl`);
393
+ args.push("--session", sessionPath);
394
+ }
309
395
 
310
- // Sub-agents don't need high thinking - off for speed
311
- // (system prompt + task text provides enough context)
312
- args.push("--thinking", "off");
396
+ if (finalNoContext) args.push("-nc");
397
+ if (finalNoExtensions) args.push("-ne");
398
+
399
+ args.push("--mode", finalMode);
400
+ args.push("--thinking", finalThinking);
313
401
 
314
402
  // Only grant the tools the agent needs
315
- if (agent.tools && agent.tools.length > 0) {
316
- args.push("--tools", agent.tools.join(","));
403
+ if (finalTools && finalTools.length > 0) {
404
+ args.push("--tools", finalTools.join(","));
317
405
  }
318
406
 
319
- // Append agent system prompt
407
+ // Append agent system prompt (from AgentDef markdown body)
320
408
  if (systemPrompt) {
321
409
  args.push("--append-system-prompt", systemPrompt);
322
410
  }
323
411
 
412
+ // Caller override system prompt (appended after agent's own)
413
+ if (override.appendSystemPrompt) {
414
+ args.push("--append-system-prompt", override.appendSystemPrompt);
415
+ }
416
+
324
417
  // Auto-load append.system.md (global or project-level)
325
418
  // Fix #3: Sub-agents now follow append.system.md instructions
326
419
  try {
@@ -332,6 +425,11 @@ export async function spawnSubagent(
332
425
  // fail silently
333
426
  }
334
427
 
428
+ // Extra raw args appended last (highest priority)
429
+ if (finalExtraArgs && finalExtraArgs.length > 0) {
430
+ args.push(...finalExtraArgs);
431
+ }
432
+
335
433
  // The task itself
336
434
  args.push(task);
337
435
 
@@ -599,6 +697,14 @@ export default function (pi: ExtensionAPI) {
599
697
  parameters: Type.Object({
600
698
  agent: Type.String({ description: "Name of the agent to invoke" }),
601
699
  task: Type.String({ description: "Task description to delegate" }),
700
+ // Optional overrides (pass-through to SubagentArgs)
701
+ thinkingLevel: Type.Optional(Type.String({ description: "Override thinking level: off | low | medium | high | xhigh" })),
702
+ session: Type.Optional(Type.Boolean({ description: "Override session persistence" })),
703
+ noContext: Type.Optional(Type.Boolean({ description: "Override -nc (skip AGENTS.md/CLAUDE.md)" })),
704
+ noExtensions: Type.Optional(Type.Boolean({ description: "Override -ne (disable extensions)" })),
705
+ mode: Type.Optional(Type.String({ description: "Override output mode: json | text" })),
706
+ tools: Type.Optional(Type.String({ description: "Override tool whitelist (comma-separated)" })),
707
+ extraArgs: Type.Optional(Type.String({ description: "Extra raw CLI args (space-separated)" })),
602
708
  }),
603
709
  async execute(toolCallId, params, signal, onUpdate, ctx) {
604
710
  const agent = agents.find((a) => a.name === params.agent);
@@ -616,6 +722,16 @@ export default function (pi: ExtensionAPI) {
616
722
  // Fix #3: Report progress via onUpdate
617
723
  onUpdate?.({ state: "running", message: `🤖 正在委派 ${agent.name} 处理...` });
618
724
 
725
+ // Build SubagentArgs from optional tool params
726
+ const toolArgs: SubagentArgs = {};
727
+ if (params.thinkingLevel !== undefined) toolArgs.thinkingLevel = params.thinkingLevel as string;
728
+ if (params.session !== undefined) toolArgs.session = params.session as boolean;
729
+ if (params.noContext !== undefined) toolArgs.noContext = params.noContext as boolean;
730
+ if (params.noExtensions !== undefined) toolArgs.noExtensions = params.noExtensions as boolean;
731
+ if (params.mode !== undefined) toolArgs.mode = params.mode as string;
732
+ if (params.tools !== undefined) toolArgs.tools = (params.tools as string).split(",").map(t => t.trim()).filter(Boolean);
733
+ if (params.extraArgs !== undefined) toolArgs.extraArgs = (params.extraArgs as string).split(/\s+/).filter(Boolean);
734
+
619
735
  const result = await spawnSubagent(
620
736
  agent,
621
737
  params.task,
@@ -625,6 +741,7 @@ export default function (pi: ExtensionAPI) {
625
741
  (progress) => {
626
742
  onUpdate?.({ state: "running", message: progress });
627
743
  },
744
+ Object.keys(toolArgs).length > 0 ? toolArgs : undefined,
628
745
  );
629
746
  const output = extractFinalOutput(result.output);
630
747
  const dur = (result.durationMs / 1000).toFixed(1);
@@ -309,6 +309,8 @@ export function uiInput(
309
309
  export interface WorkflowSubStepWidgetState {
310
310
  agent: string;
311
311
  status: "pending" | "running" | "done" | "failed";
312
+ /** Thinking level for this agent ("off" | "low" | "medium" | "high" | "xhigh") */
313
+ thinkingLevel?: string;
312
314
  /** Recent tool activity (e.g. "edit:src/main.rs", "read:config.json") */
313
315
  tools?: string[];
314
316
  /** Output file paths */
@@ -357,6 +359,8 @@ export interface WorkflowWidgetState {
357
359
  updatedAt: string;
358
360
  /** Human-readable task summary shown in widget header, e.g. "[feat - 在 auth 中实现登录]" */
359
361
  taskSummary?: string;
362
+ /** Workflow UUID for cross-file traceability */
363
+ workflowId?: string;
360
364
  }
361
365
 
362
366
  // ── Widget component builder ─────────────────────────────────
@@ -440,6 +444,9 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
440
444
  ? theme.fg("error", "✗")
441
445
  : theme.fg("warning", "■");
442
446
  lines.push(`${glyph} 工作流 · ${dim(theme, modeLabel)} · ${dim(theme, formatDurationFull(elapsed))}`);
447
+ if (state.workflowId) {
448
+ lines.push(` ${dim(theme, `UUID: ${state.workflowId}`)}`);
449
+ }
443
450
 
444
451
  // ── Step list ──
445
452
  for (let i = 0; i < state.steps.length; i++) {
@@ -466,20 +473,6 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
466
473
  icon = dim(theme, "◦");
467
474
  }
468
475
 
469
- // ── Duration ──
470
- let displayDurMs: number | undefined = s.durationMs;
471
- if (isRunning && s.startedAt) {
472
- displayDurMs = Date.now() - s.startedAt;
473
- }
474
- const durStr =
475
- displayDurMs != null
476
- ? dim(theme, ` (${formatDurationFull(displayDurMs)}`)
477
- : isRunning
478
- ? dim(theme, ` (0s`)
479
- : "";
480
- const timeout = s.timeoutMs ? dim(theme, `/超时时间${formatTimeout(s.timeoutMs)}`) : "";
481
- const durClose = displayDurMs != null || isRunning ? dim(theme, ")") : "";
482
-
483
476
  // ── Loop count (第 N 次循环) for loop-group steps ──
484
477
  let loopStr = "";
485
478
  if (s.loopCount != null && s.loopCount > 0) {
@@ -520,8 +513,8 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
520
513
  stepIndent = " ";
521
514
  }
522
515
 
523
- // ── Step line ──
524
- lines.push(`${stepIndent}${icon} ${labelStyle}${loopStr}${durStr}${timeout}${durClose}`);
516
+ // ── Step line (注意:计时/超时信息只在子代理行展示,不在父步骤行展示) ──
517
+ lines.push(`${stepIndent}${icon} ${labelStyle}${loopStr}`);
525
518
 
526
519
  // ── Sub-steps (agents with |__ tree) ──
527
520
  if (s.subSteps && s.subSteps.length > 0) {
@@ -550,11 +543,14 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
550
543
  let subDurStr = "";
551
544
  let subTimeoutStr = "";
552
545
  let subDurClose = "";
546
+ // ⭐ 修复:已完成/失败的子代理使用记录的 durationMs,运行中使用 live 计时
553
547
  let elapsedMs: number | undefined;
554
- if (sub.startedAt) {
555
- elapsedMs = Date.now() - sub.startedAt;
556
- } else if (sub.durationMs != null) {
548
+ if (isSubDone || sub.status === "failed") {
549
+ // 已完成/失败 → 使用最终记录的 durationMs(代理完成时已冻结)
557
550
  elapsedMs = sub.durationMs;
551
+ } else if (isSubRunning && sub.startedAt) {
552
+ // 运行中 → 实时计算从 startedAt 到现在的时长
553
+ elapsedMs = Date.now() - sub.startedAt;
558
554
  }
559
555
  if (elapsedMs != null) {
560
556
  subDurStr = dim(theme, ` (${formatDurationFull(elapsedMs)}`);
@@ -570,7 +566,8 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
570
566
  subDurClose = dim(theme, ")");
571
567
  }
572
568
  const agentConnector = dim(theme, "|__");
573
- lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent} ·${subDurStr}${subTimeoutStr}${subDurClose}`);
569
+ const thinkingTag = sub.thinkingLevel ? dim(theme, `thinking-${sub.thinkingLevel} `) : "";
570
+ lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent} · ${thinkingTag}${subDurStr}${subTimeoutStr}${subDurClose}`);
574
571
 
575
572
  // ── Children (tools, outputs, or "正在排队") ──
576
573
  const childItems: string[] = [];
@@ -1011,6 +1008,7 @@ export function sendWorkflowResult(
1011
1008
  `[${taskSummary}]`,
1012
1009
  "",
1013
1010
  `${resultIcon} **工作流${statusText}** (${totalDur})`,
1011
+ state.workflowId ? `工作流 UUID: \`${state.workflowId}\`` : "",
1014
1012
  "",
1015
1013
  stepSummaryParts.join("\n"),
1016
1014
  "",
@@ -1058,6 +1056,7 @@ export function buildWidgetState(
1058
1056
  status: WorkflowWidgetState["status"],
1059
1057
  extra?: { toolCount?: number; tokenCount?: number },
1060
1058
  taskSummary?: string,
1059
+ workflowId?: string,
1061
1060
  ): WorkflowWidgetState {
1062
1061
  return {
1063
1062
  mode,
@@ -1069,6 +1068,7 @@ export function buildWidgetState(
1069
1068
  toolCount: extra?.toolCount,
1070
1069
  tokenCount: extra?.tokenCount,
1071
1070
  taskSummary,
1071
+ workflowId,
1072
1072
  };
1073
1073
  }
1074
1074