@tea-agent/loop-agent 0.27.1-beta.2 → 0.28.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 (48) hide show
  1. package/CHANGELOG.md +40 -1
  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 +165 -56
  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/worker/observability/read-model.js +66 -8
  14. package/dist/worker/observe/static/dag-model.js +85 -13
  15. package/dist/workflows/dag/dynamic-runtime/loop-until.js +4 -0
  16. package/dist/workflows/dag/dynamic-runtime/map.js +13 -13
  17. package/dist/workflows/dag/frontend-implementation-contract.js +6 -124
  18. package/dist/workflows/dag/frontend-prewrite-gate.js +5 -35
  19. package/dist/workflows/dag/frontend-test-case-checklist.js +201 -8
  20. package/dist/workflows/dag/frontend-test-result-contract.js +52 -3
  21. package/dist/workflows/dag/init-hybrid.js +154 -95
  22. package/dist/workflows/dag/lifecycle.js +33 -2
  23. package/dist/workflows/dag/node-execution.js +11 -5
  24. package/dist/workflows/dag/output-protocol.js +25 -106
  25. package/dist/workflows/dag/report.js +9 -2
  26. package/dist/workflows/dag/rerun-run.js +62 -3
  27. package/dist/workflows/dag/run-store.js +6 -1
  28. package/dist/workflows/dag/runner.js +15 -3
  29. package/dist/workflows/dag/types.js +27 -0
  30. package/dist/workflows/dag/validate.js +121 -1
  31. package/docs/architecture/runtime-boundaries.md +13 -11
  32. package/docs/init-surface.manifest.json +6 -2
  33. package/docs/templates/README.md +9 -1
  34. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  35. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +14 -7
  36. package/docs/templates/frontend-test-dag.json +55 -15
  37. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -9
  38. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  39. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  40. package/docs/templates/frontend-test-dag.review-execution.prompt.md +1 -1
  41. package/harness.json +4 -4
  42. package/package.json +1 -1
  43. package/skills/loop-agent/SKILL.md +1 -1
  44. package/skills/loop-agent/references/command-reference.md +18 -6
  45. package/skills/playwright-cli/SKILL.md +69 -402
  46. package/skills/playwright-cli/references/tracing.md +3 -137
  47. package/skills/playwright-cli/references/video-recording.md +3 -141
  48. 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,19 +681,32 @@ 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
- // The workspace diff is authoritative. The model outcome is advisory and
580
- // may be missing, malformed, or inconsistent without blocking a completed
581
- // writer. An explicit blocked signal remains a hard failure.
582
- if (parsed.candidates.some((candidate) => candidate.outcome === "blocked")) {
684
+ if (parsed.kind !== "valid") {
583
685
  return {
584
686
  ok: false,
585
- reason: `writer outcome validation failed: explicit blocked outcome; ${diagnostics}`,
687
+ reason: `writer outcome validation failed: ${parsed.kind} outcome; ${diagnostics}; expected IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked`,
586
688
  };
587
689
  }
588
- return {
589
- ok: true,
590
- outcome: changedFiles.length > 0 ? "changed" : "already-satisfied",
591
- };
690
+ const outcome = parsed.outcome;
691
+ if (outcome === "blocked") {
692
+ return {
693
+ ok: false,
694
+ reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
695
+ };
696
+ }
697
+ if (outcome === "changed" && changedFiles.length === 0) {
698
+ return {
699
+ ok: false,
700
+ reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
701
+ };
702
+ }
703
+ if (outcome === "already-satisfied" && changedFiles.length > 0) {
704
+ return {
705
+ ok: false,
706
+ reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
707
+ };
708
+ }
709
+ return { ok: true, outcome };
592
710
  }
593
711
  function parseWriterImplementationOutcome(text) {
594
712
  const lines = text.split(/\r?\n/);
@@ -719,15 +837,6 @@ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
719
837
  const value = normalizedProtocolValue(direct[1] ?? "");
720
838
  return value ? `${firstProtocolLine} ${value}` : undefined;
721
839
  }
722
- // Writers sometimes place a short delivery sentence before the protocol
723
- // line. Accept the protocol token when it appears inline, while preserving
724
- // the candidate value so template repetitions still fail as unknown or
725
- // conflicting outcomes.
726
- const inline = normalizedLine.match(new RegExp(`${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
727
- if (inline) {
728
- const value = normalizedProtocolValue(inline[1] ?? "");
729
- return value ? `${firstProtocolLine} ${value}` : undefined;
730
- }
731
840
  const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
732
841
  if (splitValue && nextLine !== undefined) {
733
842
  const value = normalizedProtocolValue(nextLine);