@tea-agent/loop-agent 0.27.1 → 0.28.1-beta.1

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 (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/application/task-lifecycle/observe.js +5 -0
  3. package/dist/application/task-lifecycle/plan-transitions.js +7 -2
  4. package/dist/cli/program.js +1 -1
  5. package/dist/commands/client-recovery.js +439 -20
  6. package/dist/commands/init.js +42 -6
  7. package/dist/executors/dag-pi-executor.js +161 -60
  8. package/dist/executors/pi-playwright-cli-tool.js +955 -0
  9. package/dist/executors/pi-sdk-executor.js +56 -0
  10. package/dist/executors/playwright-cli-launcher.js +63 -0
  11. package/dist/executors/shell-executor.js +128 -0
  12. package/dist/shared/playwright-cli-command-policy.js +41 -0
  13. package/dist/task/task-demand-routing.js +1 -15
  14. package/dist/worker/observability/read-model.js +66 -8
  15. package/dist/worker/observe/static/dag-model.js +85 -13
  16. package/dist/workflows/dag/dynamic-runtime/loop-until.js +4 -0
  17. package/dist/workflows/dag/dynamic-runtime/map.js +13 -13
  18. package/dist/workflows/dag/frontend-implementation-contract.js +124 -6
  19. package/dist/workflows/dag/frontend-prewrite-gate.js +22 -8
  20. package/dist/workflows/dag/frontend-test-case-checklist.js +201 -8
  21. package/dist/workflows/dag/frontend-test-result-contract.js +52 -3
  22. package/dist/workflows/dag/init-hybrid.js +181 -68
  23. package/dist/workflows/dag/lifecycle.js +33 -2
  24. package/dist/workflows/dag/node-execution.js +11 -5
  25. package/dist/workflows/dag/output-protocol.js +48 -83
  26. package/dist/workflows/dag/report.js +9 -2
  27. package/dist/workflows/dag/rerun-run.js +62 -3
  28. package/dist/workflows/dag/run-store.js +6 -1
  29. package/dist/workflows/dag/runner.js +15 -3
  30. package/dist/workflows/dag/types.js +27 -0
  31. package/dist/workflows/dag/validate.js +121 -1
  32. package/docs/architecture/runtime-boundaries.md +13 -11
  33. package/docs/init-surface.manifest.json +6 -2
  34. package/docs/templates/README.md +9 -1
  35. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  36. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +14 -7
  37. package/docs/templates/frontend-test-dag.json +55 -15
  38. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -9
  39. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  40. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  41. package/docs/templates/frontend-test-dag.review-execution.prompt.md +1 -1
  42. package/harness.json +1 -1
  43. package/package.json +1 -1
  44. package/skills/loop-agent/SKILL.md +1 -1
  45. package/skills/loop-agent/references/command-reference.md +18 -6
  46. package/skills/playwright-cli/SKILL.md +69 -402
  47. package/skills/playwright-cli/references/tracing.md +3 -137
  48. package/skills/playwright-cli/references/video-recording.md +3 -141
  49. package/skills/playwright-cli-case-generator/SKILL.md +53 -46
@@ -6,7 +6,7 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
8
8
  import { loadHarnessManifest } from "../governance/harness.js";
9
- import { OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, applyPiRetryMerge, buildOpenCodeTransientRetryPluginSource, inspectPiRetryConfig, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
9
+ import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, applyPiRetryMerge, buildOpenCodeContextOverflowCompactPluginSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectPiRetryConfig, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
10
10
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
11
11
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
12
12
  const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
@@ -1243,6 +1243,8 @@ function inferInitSurfaceMode(relativePath) {
1243
1243
  }
1244
1244
  if (relativePath === "harness.json" ||
1245
1245
  relativePath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH ||
1246
+ relativePath === OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH ||
1247
+ relativePath === PI_CONTEXT_OVERFLOW_EXTENSION_PATH ||
1246
1248
  relativePath.startsWith("scripts/") ||
1247
1249
  relativePath.startsWith(".harness/prompts/") ||
1248
1250
  relativePath.startsWith("docs/README.md") ||
@@ -1293,6 +1295,12 @@ async function buildDesiredSurfaceContent(input) {
1293
1295
  if (manifestPath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH) {
1294
1296
  return { content: buildOpenCodeTransientRetryPluginSource() };
1295
1297
  }
1298
+ if (manifestPath === OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH) {
1299
+ return { content: buildOpenCodeContextOverflowCompactPluginSource() };
1300
+ }
1301
+ if (manifestPath === PI_CONTEXT_OVERFLOW_EXTENSION_PATH) {
1302
+ return { content: buildPiContextOverflowExtensionSource() };
1303
+ }
1296
1304
  if (manifestPath.startsWith("scripts/")) {
1297
1305
  const scripts = buildInitScriptFiles(input.governanceRoot);
1298
1306
  return { content: scripts[manifestPath] };
@@ -2465,11 +2473,25 @@ export async function initializeLoopAgentProject(options) {
2465
2473
  repoRoot,
2466
2474
  mode: clientRecoveryMode,
2467
2475
  });
2468
- if (clientRecovery?.plugin.written) {
2469
- written.push(OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH);
2470
- }
2471
- else if (clientRecoveryMode === "off") {
2472
- skipped.push(OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH);
2476
+ const clientRecoveryPaths = [
2477
+ OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2478
+ OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2479
+ PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2480
+ ];
2481
+ if (clientRecoveryMode === "off") {
2482
+ for (const recoveryPath of clientRecoveryPaths)
2483
+ skipped.push(recoveryPath);
2484
+ }
2485
+ else if (clientRecovery) {
2486
+ const installResults = [
2487
+ clientRecovery.plugin,
2488
+ clientRecovery.overflowPlugin,
2489
+ clientRecovery.piExtension,
2490
+ ];
2491
+ for (const [index, recoveryPath] of clientRecoveryPaths.entries()) {
2492
+ if (installResults[index]?.written)
2493
+ written.push(recoveryPath);
2494
+ }
2473
2495
  }
2474
2496
  await writeInitSurfaceState({
2475
2497
  repoRoot,
@@ -2562,6 +2584,14 @@ function recommendedNextFor(report) {
2562
2584
  next.push("No init update actions are needed.");
2563
2585
  return next;
2564
2586
  }
2587
+ const CLIENT_RECOVERY_SURFACE_PATHS = new Set([
2588
+ OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2589
+ OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2590
+ PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2591
+ ]);
2592
+ function isClientRecoverySurfacePath(pathName) {
2593
+ return CLIENT_RECOVERY_SURFACE_PATHS.has(pathName);
2594
+ }
2565
2595
  export async function checkInitUpdate(input) {
2566
2596
  const repoRoot = path.resolve(input.repoRoot);
2567
2597
  const { projectName, governanceRoot } = await resolveInitProjectContext({
@@ -2569,6 +2599,8 @@ export async function checkInitUpdate(input) {
2569
2599
  projectName: input.projectName,
2570
2600
  governanceRoot: input.governanceRoot,
2571
2601
  });
2602
+ const clientRecoveryMode = input.clientRecovery ?? "auto";
2603
+ const skipClientRecoverySurfaces = clientRecoveryMode === "off";
2572
2604
  const recordedState = await readExistingSurfaceState(repoRoot);
2573
2605
  const assetRoot = await findPackageRoot();
2574
2606
  const surfaceState = recordedState?.stateKind ?? "missing";
@@ -2608,6 +2640,10 @@ export async function checkInitUpdate(input) {
2608
2640
  });
2609
2641
  }
2610
2642
  for (const [pathName, state] of Object.entries(currentState.files)) {
2643
+ // Mirror fresh init: client-recovery=off never reinstalls recovery surfaces.
2644
+ if (skipClientRecoverySurfaces && isClientRecoverySurfacePath(pathName)) {
2645
+ continue;
2646
+ }
2611
2647
  if (state.relationship === "missing-from-target") {
2612
2648
  if (retiredTargetPaths.has(pathName))
2613
2649
  continue;
@@ -1,8 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
4
5
  import { executePiStep, } from "./pi-executor.js";
5
6
  import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
7
+ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
8
+ import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
6
9
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
7
10
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
8
11
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
@@ -122,12 +125,16 @@ export function resolveDagPiStepName(task) {
122
125
  }
123
126
  return resolvePiExecutorStep(resolveDagPiPersona(task));
124
127
  }
125
- /** Pi DAG nodes are read-only unless they explicitly opt into the bounded write tool profile. */
126
128
  export function resolveDagPiToolNames(task) {
127
- if (isDagPiWriteTask(task)) {
128
- return [...DAG_PI_WRITE_TOOLS];
129
- }
130
- return [...DAG_PI_READONLY_TOOLS];
129
+ if (!isDagPiWriteTask(task))
130
+ return [...DAG_PI_READONLY_TOOLS];
131
+ const tools = [...DAG_PI_WRITE_TOOLS];
132
+ // Pi SDK activates custom tools only when they are in this explicit list.
133
+ // Do not add a command capability to ordinary writers or read-only nodes.
134
+ if (dagCommandPolicyAllows(task.commandPolicy, "playwright-cli")) {
135
+ tools.push("playwright_cli");
136
+ }
137
+ return tools;
131
138
  }
132
139
  export function buildDagPiUserMessage(task, persona, step) {
133
140
  const role = task.role ?? "unspecified";
@@ -258,6 +265,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
258
265
  await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
259
266
  return { promptPath, summaryPath };
260
267
  }
268
+ async function resolveControllerOwnedFrontendBaseUrl(runDir) {
269
+ const capabilityPath = path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json");
270
+ let capability;
271
+ try {
272
+ capability = JSON.parse(await readFile(capabilityPath, "utf8"));
273
+ }
274
+ catch {
275
+ throw new Error("browser-command-capability-unavailable: missing controller-owned frontend browser capability artifact");
276
+ }
277
+ if (capability.status !== "ready" ||
278
+ typeof capability.baseUrl !== "string") {
279
+ throw new Error("browser-command-capability-unavailable: invalid controller-owned frontend browser capability artifact");
280
+ }
281
+ let parsed;
282
+ try {
283
+ parsed = new URL(capability.baseUrl);
284
+ }
285
+ catch {
286
+ throw new Error("browser-command-capability-unavailable: invalid controller-owned frontend browser origin");
287
+ }
288
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
289
+ parsed.username ||
290
+ parsed.password ||
291
+ parsed.search ||
292
+ parsed.hash ||
293
+ /(?:^|\.)(?:www\.)?[^.]*(?:prod|production)/i.test(parsed.hostname)) {
294
+ throw new Error("browser-command-capability-unavailable: unsafe controller-owned frontend browser origin");
295
+ }
296
+ return parsed.toString();
297
+ }
261
298
  const DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES = {
262
299
  readGitStatusPorcelain,
263
300
  recoverRootNulArtifact,
@@ -315,52 +352,120 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
315
352
  }
316
353
  : undefined;
317
354
  let writerToolPolicy;
318
- if (isWriteTask) {
355
+ const commandPolicy = resolveDagCommandPolicy(input.task.commandPolicy);
356
+ const allowsPlaywrightCli = dagCommandPolicyAllows(input.task.commandPolicy, "playwright-cli");
357
+ let playwrightToolContext;
358
+ if (allowsPlaywrightCli && !isWriteTask) {
359
+ return {
360
+ ok: false, stdout: "", stderr: "pi command capability requires an explicit bounded write tool profile", failureCategory: "tool-policy", durationMs: Date.now() - started,
361
+ };
362
+ }
363
+ if (isWriteTask || allowsPlaywrightCli) {
319
364
  try {
320
- const policyContext = buildPiWriterToolPolicyContext({
321
- repoRoot: input.cwd,
322
- allowedPaths: input.task.allowedPaths,
323
- writeSet: input.task.writeSet,
324
- forbiddenPaths: input.task.forbiddenPaths,
325
- writePolicy: input.task.writePolicy,
326
- });
327
- const customTools = await createPiWriterCustomTools(policyContext);
328
- writerToolPolicy = { requireSdk: true, customTools };
365
+ const customTools = [];
366
+ if (isWriteTask) {
367
+ const policyContext = buildPiWriterToolPolicyContext({
368
+ repoRoot: input.cwd,
369
+ allowedPaths: input.task.allowedPaths,
370
+ writeSet: input.task.writeSet,
371
+ forbiddenPaths: input.task.forbiddenPaths,
372
+ writePolicy: input.task.writePolicy,
373
+ });
374
+ customTools.push(...(await createPiWriterCustomTools(policyContext)));
375
+ }
376
+ if (commandPolicy.mode === "capability-allowlist") {
377
+ for (const capability of commandPolicy.capabilities) {
378
+ const factory = PI_COMMAND_CAPABILITY_REGISTRY[capability];
379
+ if (!factory) {
380
+ throw new Error(`unknown command capability: ${capability}`);
381
+ }
382
+ if (capability === "playwright-cli") {
383
+ const caseId = resolveCaseIdFromWriteSet(input.task.writeSet) ??
384
+ input.task.id;
385
+ const evidenceDir = resolveEvidenceDirFromWriteSet(input.task.writeSet) ??
386
+ `testcase/frontend/evidence/${caseId}`;
387
+ playwrightToolContext = {
388
+ repoRoot: input.cwd,
389
+ runDir: meta.runDir,
390
+ nodeId: input.task.id,
391
+ caseId,
392
+ evidenceDir,
393
+ baseUrl: await resolveControllerOwnedFrontendBaseUrl(meta.runDir),
394
+ inputRoot: "testcase/frontend/fixtures",
395
+ };
396
+ // Cleanup must be confirmed before a new case; otherwise default-session
397
+ // isolation is unknown and no Pi invocation is permitted.
398
+ const preCleanup = await cleanupPlaywrightCliDefaultSession(playwrightToolContext, undefined, "pre-start");
399
+ if (!preCleanup.ok) {
400
+ throw new Error(`browser-command-capability-unavailable: pre-start cleanup failed (${preCleanup.errorClass ?? "unknown"})`);
401
+ }
402
+ customTools.push(await createPlaywrightCliTool(playwrightToolContext));
403
+ }
404
+ else {
405
+ customTools.push(await factory(playwrightToolContext));
406
+ }
407
+ }
408
+ }
409
+ if (customTools.length > 0) {
410
+ writerToolPolicy = { requireSdk: true, customTools };
411
+ }
329
412
  }
330
413
  catch (error) {
331
414
  return {
332
415
  ok: false,
333
416
  stdout: "",
334
- stderr: `pi writer tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
417
+ stderr: `pi writer/command tool policy unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
335
418
  failureCategory: "tool-policy",
336
419
  durationMs: Date.now() - started,
337
420
  };
338
421
  }
339
422
  }
340
- const result = await piStepFn({
341
- attachedFiles: [],
342
- modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
343
- prompt: input.prompt,
344
- repoRoot: input.cwd,
345
- step,
346
- toolNames: resolveDagPiToolNames(input.task),
347
- userMessage: buildDagPiUserMessage(input.task, persona, step),
348
- sessionEventsPath: path.join(meta.runDir, input.task.id, "session-events.jsonl"),
349
- // Absolute max wall clock from adaptive liveness policy (defaults to 4h).
350
- timeoutMs: input.timeoutMs,
351
- stallTimeoutMs: input.stallTimeoutMs,
352
- abortGraceMs: input.abortGraceMs,
353
- ...(writerToolPolicy ? { writerToolPolicy } : {}),
354
- onActivity: bridgeActivity
355
- ? (activity) => {
356
- if (activity.kind === "lease" ||
357
- activity.kind === "synthetic-heartbeat") {
358
- return;
423
+ let result;
424
+ try {
425
+ result = await piStepFn({
426
+ attachedFiles: [],
427
+ modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
428
+ prompt: input.prompt,
429
+ repoRoot: input.cwd,
430
+ step,
431
+ toolNames: resolveDagPiToolNames(input.task),
432
+ userMessage: buildDagPiUserMessage(input.task, persona, step),
433
+ sessionEventsPath: path.join(meta.runDir, input.task.id, "session-events.jsonl"),
434
+ // Absolute max wall clock from adaptive liveness policy (defaults to 4h).
435
+ timeoutMs: input.timeoutMs,
436
+ stallTimeoutMs: input.stallTimeoutMs,
437
+ abortGraceMs: input.abortGraceMs,
438
+ ...(writerToolPolicy ? { writerToolPolicy } : {}),
439
+ onActivity: bridgeActivity
440
+ ? (activity) => {
441
+ if (activity.kind === "lease" ||
442
+ activity.kind === "synthetic-heartbeat") {
443
+ return;
444
+ }
445
+ bridgeActivity(activity.kind, activity.at);
359
446
  }
360
- bridgeActivity(activity.kind, activity.at);
447
+ : undefined,
448
+ });
449
+ }
450
+ catch (error) {
451
+ if (playwrightToolContext) {
452
+ try {
453
+ await cleanupPlaywrightCliDefaultSession(playwrightToolContext, undefined, "post-execution");
361
454
  }
362
- : undefined,
363
- });
455
+ catch {
456
+ // best-effort
457
+ }
458
+ }
459
+ throw error;
460
+ }
461
+ if (playwrightToolContext) {
462
+ try {
463
+ await cleanupPlaywrightCliDefaultSession(playwrightToolContext, undefined, "post-execution");
464
+ }
465
+ catch {
466
+ // best-effort controller cleanup must not mask the primary result
467
+ }
468
+ }
364
469
  const context = {
365
470
  channel: "dag",
366
471
  cwd: input.cwd,
@@ -576,32 +681,19 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
576
681
  export function validateWriterImplementationOutcome(text, changedFiles) {
577
682
  const parsed = parseWriterImplementationOutcome(text);
578
683
  const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
579
- if (parsed.kind !== "valid") {
580
- return {
581
- ok: false,
582
- reason: `writer outcome validation failed: ${parsed.kind} outcome; ${diagnostics}; expected IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked`,
583
- };
584
- }
585
- const outcome = parsed.outcome;
586
- if (outcome === "blocked") {
587
- return {
588
- ok: false,
589
- reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
590
- };
591
- }
592
- if (outcome === "changed" && changedFiles.length === 0) {
593
- return {
594
- ok: false,
595
- reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
596
- };
597
- }
598
- if (outcome === "already-satisfied" && changedFiles.length > 0) {
684
+ // The workspace diff is authoritative. The model outcome is advisory and
685
+ // may be missing, malformed, or inconsistent without blocking a completed
686
+ // writer. An explicit blocked signal remains a hard failure.
687
+ if (parsed.candidates.some((candidate) => candidate.outcome === "blocked")) {
599
688
  return {
600
689
  ok: false,
601
- reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
690
+ reason: `writer outcome validation failed: explicit blocked outcome; ${diagnostics}`,
602
691
  };
603
692
  }
604
- return { ok: true, outcome };
693
+ return {
694
+ ok: true,
695
+ outcome: changedFiles.length > 0 ? "changed" : "already-satisfied",
696
+ };
605
697
  }
606
698
  function parseWriterImplementationOutcome(text) {
607
699
  const lines = text.split(/\r?\n/);
@@ -732,6 +824,15 @@ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
732
824
  const value = normalizedProtocolValue(direct[1] ?? "");
733
825
  return value ? `${firstProtocolLine} ${value}` : undefined;
734
826
  }
827
+ // Writers sometimes place a short delivery sentence before the protocol
828
+ // line. Accept the protocol token when it appears inline, while preserving
829
+ // the candidate value so template repetitions still fail as unknown or
830
+ // conflicting outcomes.
831
+ const inline = normalizedLine.match(new RegExp(`${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
832
+ if (inline) {
833
+ const value = normalizedProtocolValue(inline[1] ?? "");
834
+ return value ? `${firstProtocolLine} ${value}` : undefined;
835
+ }
735
836
  const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
736
837
  if (splitValue && nextLine !== undefined) {
737
838
  const value = normalizedProtocolValue(nextLine);