@tea-agent/loop-agent 0.38.0 → 0.39.0-next.10

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 (105) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/commands/init.js +14 -2
  5. package/dist/executors/dag-pi-executor.js +1 -0
  6. package/dist/governance/manifest-types.js +2 -0
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/task/config-types.js +21 -0
  10. package/dist/task/frontend-project-capability.js +292 -38
  11. package/dist/worker/console/chat/turn-process.js +7 -20
  12. package/dist/worker/console/harness-pi-model-matrix.js +146 -0
  13. package/dist/worker/console/operator-actions.js +49 -0
  14. package/dist/worker/console/operator-user-error.js +19 -0
  15. package/dist/worker/console/pi-readiness.js +7 -1
  16. package/dist/worker/console/security.js +20 -1
  17. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-B1E-cK2K.js} +1 -1
  18. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-BGzjO9rd.js} +1 -1
  19. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DLdhmjAc.js} +1 -1
  20. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-MYV8D865.js} +1 -1
  21. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BzDorEwd.js} +1 -1
  22. package/dist/worker/console/static/assets/channel-jVvdIfCV.js +1 -0
  23. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-C1X8DD9m.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-_b3zpUSR.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-B1nq6qNo.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BHy2jQUr.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BDoLBPGY.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-SDhf5BHa.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-B2X9WLQ5.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Y1pEsL6r.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-BhlwtGnw.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-lFslfIU9.js} +1 -1
  33. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-VqdMg0Mg.js +1 -0
  34. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-VqdMg0Mg.js +1 -0
  35. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-ms_QSyzO.js} +1 -1
  36. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DVBoJPNo.js} +1 -1
  37. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-ClesFZ8l.js} +1 -1
  38. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-CwcFFdXS.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-CK6fAQka.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CxBy8J_n.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-Cn6E66dO.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-Cep_FINW.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-gzv9ZY8Y.js} +1 -1
  44. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CZVlOlaj.js} +1 -1
  45. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-EGJvKAH5.js} +1 -1
  46. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-DhfPgO79.js} +1 -1
  47. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BzJfLMVN.js} +1 -1
  48. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-C5uHg3cm.js} +1 -1
  49. package/dist/worker/console/static/assets/{index-DQxiso1w.js → index-0DWC8Vk9.js} +67 -67
  50. package/dist/worker/console/static/assets/index-DAouGklf.css +1 -0
  51. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-BFEs1rSc.js} +1 -1
  52. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-DOY3aNcc.js} +1 -1
  53. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-CvEVBETJ.js} +1 -1
  54. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-CvRuqo46.js} +1 -1
  55. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-B8zO1HYx.js} +1 -1
  56. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-FLK4IOPu.js} +5 -5
  57. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-CwQ-RWrj.js} +1 -1
  58. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-H_S6wWna.js} +1 -1
  59. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CZUOcDQQ.js} +1 -1
  60. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CeaPaSv9.js} +1 -1
  61. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-Cpcs9ua-.js} +1 -1
  62. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D0_Aeyk6.js} +1 -1
  63. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-73kWjUq_.js} +1 -1
  64. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-41K29Ot4.js} +1 -1
  65. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-D1nO0oh9.js} +1 -1
  66. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-C77XHgDB.js} +1 -1
  67. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B9GimbZV.js +1 -0
  68. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DC0D7jEO.js} +2 -2
  69. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-yxfxvdqm.js +8 -0
  70. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-BLDHd_Uf.js} +1 -1
  71. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-eh6Y_q96.js} +1 -1
  72. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-Cj4Cy9tr.js} +1 -1
  73. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-DUIjXY1m.js} +1 -1
  74. package/dist/worker/console/static/index.html +2 -2
  75. package/dist/worker/console/static-src/app/console-types.js +51 -0
  76. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +14 -2
  77. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  78. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  79. package/dist/workflows/dag/failure-routing.js +9 -4
  80. package/dist/workflows/dag/frontend-implementation-contract.js +118 -0
  81. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  82. package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
  83. package/dist/workflows/dag/init-hybrid.js +271 -26
  84. package/dist/workflows/dag/lifecycle.js +4 -0
  85. package/dist/workflows/dag/node-execution.js +37 -7
  86. package/dist/workflows/dag/report.js +6 -0
  87. package/dist/workflows/dag/retry-policy.js +15 -0
  88. package/dist/workflows/dag/types.js +49 -4
  89. package/dist/workflows/dag/validate.js +7 -2
  90. package/docs/templates/frontend-design-contract.md +40 -14
  91. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  92. package/docs/templates/frontend-task-constraints.md +40 -13
  93. package/docs/templates/frontend-task-requirement.md +54 -20
  94. package/docs/templates/harness.schema.json +8 -0
  95. package/harness.json +11 -2
  96. package/package.json +1 -1
  97. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  98. package/skills/frontend-implementation/references/design-spec.md +16 -19
  99. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  100. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  101. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  102. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  103. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  105. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -191,6 +191,42 @@ const gap = z
191
191
  blocking: z.boolean(),
192
192
  })
193
193
  .strict();
194
+ /**
195
+ * Structured component/token selection contract (ADR 0016). Every user-visible
196
+ * UI purpose the plan touches must be declared here so the prewrite gate can
197
+ * deterministically cross-check that the selection comes from the frontend
198
+ * specification (openspec/ai_workspace component + theme specs) instead of
199
+ * relying on design-review's soft model judgment.
200
+ *
201
+ * - `specified`: the frontend spec mandates this component for the purpose →
202
+ * `specReference` is required (path/section/line hit).
203
+ * - `reuse-existing`: reuse an existing repo component/convention not named by
204
+ * the spec → `specReference` may be null, rationale explains the basis.
205
+ * - `new`: neither spec nor existing code fits → `specReference` must be null,
206
+ * rationale must declare the deviation (design-review approves it).
207
+ *
208
+ * `specReference` is nullable + optional because `stripNullValuesDeep` strips
209
+ * null object values before validation, so a `new` choice's `specReference:
210
+ * null` becomes an absent field; the superRefine below treats absent and null
211
+ * identically.
212
+ */
213
+ const uiComponentSpecReferenceSchema = z
214
+ .object({
215
+ path: safePath,
216
+ section: z.string(),
217
+ line: z.number().int().nullable().optional(),
218
+ })
219
+ .strict();
220
+ const uiComponentChoiceSchema = z
221
+ .object({
222
+ purpose: z.string().min(1),
223
+ component: z.string().min(1),
224
+ decision: z.enum(["specified", "reuse-existing", "new"]),
225
+ specReference: uiComponentSpecReferenceSchema.nullable().optional(),
226
+ rationale: z.string().min(1),
227
+ })
228
+ .strict();
229
+ const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
194
230
  /**
195
231
  * Normalize unstable model emissions before the strict schema sees them:
196
232
  * recursively drop null values (object entries and array elements). A null for
@@ -316,6 +352,12 @@ export const frontendImplementationContractSchema = z
316
352
  })
317
353
  .strict()).min(1),
318
354
  evidenceGaps: z.array(gap),
355
+ implementationSteps: z.array(z.string().min(1)).optional(),
356
+ stylingStrategy: z.string().min(1).optional(),
357
+ uiComponentChoices: uiComponentChoicesSchema.optional(),
358
+ dependencyPolicy: z.string().min(1).optional(),
359
+ residualRisks: z.array(z.string().min(1)).optional(),
360
+ realIntegrationGap: z.string().min(1).optional(),
319
361
  })
320
362
  .strict()
321
363
  .superRefine((value, ctx) => {
@@ -391,6 +433,26 @@ export const frontendImplementationContractSchema = z
391
433
  path: ["uiStates"],
392
434
  });
393
435
  }
436
+ for (const [index, choice] of (value.uiComponentChoices ?? []).entries()) {
437
+ if (choice.decision === "specified") {
438
+ if (!choice.specReference || !choice.specReference.path) {
439
+ ctx.addIssue({
440
+ code: "custom",
441
+ message: "specified component choice requires a non-empty specReference.path",
442
+ path: ["uiComponentChoices", index, "specReference"],
443
+ });
444
+ }
445
+ }
446
+ else if (choice.decision === "new") {
447
+ if (choice.specReference) {
448
+ ctx.addIssue({
449
+ code: "custom",
450
+ message: "new component choice must not carry a specReference",
451
+ path: ["uiComponentChoices", index, "specReference"],
452
+ });
453
+ }
454
+ }
455
+ }
394
456
  if (value.mockApi.strategy !== "not-needed") {
395
457
  if (value.mockApi.endpoints.length === 0) {
396
458
  ctx.addIssue({
@@ -426,6 +488,44 @@ export const frontendImplementationContractSchema = z
426
488
  });
427
489
  }
428
490
  }));
491
+ function isPlainObject(value) {
492
+ return value !== null && typeof value === "object" && !Array.isArray(value);
493
+ }
494
+ /**
495
+ * Apply an RFC 7386 merge-patch on a target contract without mutating it.
496
+ * `null` removes the key; plain objects merge recursively when the target value
497
+ * is also a plain object; arrays and scalars replace the whole value. The
498
+ * result is re-validated by analyzeFrontendImplementationContract before it is
499
+ * ever materialized, so a patch that removes a required field still fails
500
+ * closed at the gate.
501
+ */
502
+ export function applyFrontendImplementationContractPatch(target, patch) {
503
+ if (!isPlainObject(patch)) {
504
+ throw new Error("frontend revision patch must be a JSON object");
505
+ }
506
+ const merge = (base, delta) => {
507
+ if (delta === null)
508
+ return undefined;
509
+ if (!isPlainObject(delta))
510
+ return delta;
511
+ const result = isPlainObject(base)
512
+ ? { ...base }
513
+ : {};
514
+ for (const [key, value] of Object.entries(delta)) {
515
+ if (value === null) {
516
+ delete result[key];
517
+ }
518
+ else if (isPlainObject(value) && isPlainObject(result[key])) {
519
+ result[key] = merge(result[key], value);
520
+ }
521
+ else {
522
+ result[key] = value;
523
+ }
524
+ }
525
+ return result;
526
+ };
527
+ return merge(target, patch);
528
+ }
429
529
  export async function assertFrontendSourceBindingFresh(input) {
430
530
  for (const source of input.binding.sources) {
431
531
  const absolute = resolveDagTaskSourcePath({
@@ -648,6 +748,18 @@ function asStringArray(value) {
648
748
  .map((item) => asString(item))
649
749
  .filter((item) => item.length > 0);
650
750
  }
751
+ /**
752
+ * Pass structured component choices through the free-form compatibility
753
+ * branch. The strict-shape branch already spreads unknown fields, so only this
754
+ * explicit constructor needs to re-emit `uiComponentChoices`; a missing/empty
755
+ * array normalizes to undefined (absent) so the optional schema field stays
756
+ * compatible with legacy free-form contracts.
757
+ */
758
+ function normalizeUiComponentChoices(value) {
759
+ if (!Array.isArray(value) || value.length === 0)
760
+ return undefined;
761
+ return value;
762
+ }
651
763
  function normalizeFrontendRoute(value) {
652
764
  const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
653
765
  if (!route || route === "." || route.split("/").includes(".."))
@@ -1331,6 +1443,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1331
1443
  },
1332
1444
  verificationTargets,
1333
1445
  evidenceGaps,
1446
+ implementationSteps: asStringArray(record.implementationSteps),
1447
+ stylingStrategy: asString(record.stylingStrategy) || undefined,
1448
+ dependencyPolicy: asString(record.dependencyPolicy) || undefined,
1449
+ residualRisks: asStringArray(record.residualRisks),
1450
+ realIntegrationGap: asString(record.realIntegrationGap) || undefined,
1451
+ uiComponentChoices: normalizeUiComponentChoices(record.uiComponentChoices),
1334
1452
  };
1335
1453
  }
1336
1454
  /**
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Deterministic Markdown renderer for a canonical frontend implementation
3
+ * contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
4
+ * next to the contract JSON, so plan prose is never produced by the model — it
5
+ * is derived from the single machine contract at materialization time.
6
+ *
7
+ * The function is pure: the same contract always renders byte-identical
8
+ * Markdown, and absent optional fields render as placeholders without throwing.
9
+ */
10
+ function bulletList(items) {
11
+ if (!items || items.length === 0)
12
+ return "";
13
+ return items.map((item) => `- ${item}`).join("\n");
14
+ }
15
+ function requirementsCoverage(contract) {
16
+ const lines = contract.requirements.map((requirement) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
17
+ return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
18
+ }
19
+ function uiStateHandling(contract) {
20
+ const lines = contract.uiStates.map((state) => {
21
+ if (state.applicable) {
22
+ return `- ${state.name}: ${state.expectedBehavior ?? ""}`;
23
+ }
24
+ return `- ${state.name}: not applicable — ${state.notApplicableReason ?? ""}`;
25
+ });
26
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
27
+ }
28
+ function interactionNotes(contract) {
29
+ const lines = contract.interactions.map((interaction) => `- ${interaction.name}: ${interaction.trigger} → ${interaction.expectedBehavior}`);
30
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
31
+ }
32
+ function verificationPlan(contract) {
33
+ const lines = contract.verificationTargets.map((target) => `- ${target.id}: ${target.commandLabel} (${target.file})`);
34
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
35
+ }
36
+ function componentSelection(contract) {
37
+ const choices = contract.uiComponentChoices ?? [];
38
+ if (choices.length === 0)
39
+ return "_(not specified)_";
40
+ return choices
41
+ .map((choice) => {
42
+ const specReference = choice.specReference;
43
+ const refText = specReference
44
+ ? ` ${specReference.path}${specReference.section ? `#${specReference.section}` : ""}${specReference.line != null ? `:${specReference.line}` : ""}`
45
+ : "";
46
+ return `- ${choice.purpose} → ${choice.component} (${choice.decision})${refText ? `:${refText}` : ""} — ${choice.rationale}`;
47
+ })
48
+ .join("\n");
49
+ }
50
+ export function renderFrontendPlanMarkdown(contract) {
51
+ return [
52
+ "# Frontend Implementation Plan",
53
+ "",
54
+ "## Requirement Coverage",
55
+ requirementsCoverage(contract),
56
+ "",
57
+ "## Implementation Steps",
58
+ bulletList(contract.implementationSteps) || "_(not specified)_",
59
+ "",
60
+ "## Target Files",
61
+ bulletList(contract.targets.files) || "_(none)_",
62
+ "",
63
+ "## UI State Handling",
64
+ uiStateHandling(contract),
65
+ "",
66
+ "## Styling & Component Strategy",
67
+ contract.stylingStrategy || "_(not specified)_",
68
+ "",
69
+ "## Component Selection (spec conformance)",
70
+ componentSelection(contract),
71
+ "",
72
+ "## Interaction Notes",
73
+ interactionNotes(contract),
74
+ "",
75
+ "## Mock & API Strategy",
76
+ contract.mockApi.strategy,
77
+ "",
78
+ "## Dependency Policy",
79
+ contract.dependencyPolicy || "_(not specified)_",
80
+ "",
81
+ "## Verification Plan",
82
+ verificationPlan(contract),
83
+ "",
84
+ "## Real Integration Gap",
85
+ contract.realIntegrationGap || "_(not specified)_",
86
+ "",
87
+ "## Residual Risks",
88
+ bulletList(contract.residualRisks) || "_(none)_",
89
+ ].join("\n") + "\n";
90
+ }