openhermes 4.3.0 → 4.11.2
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.
- package/CONTEXT.md +10 -1
- package/README.md +54 -42
- package/bootstrap.ts +396 -142
- package/harness/agents/oh-browser.md +97 -0
- package/harness/agents/oh-builder.md +78 -0
- package/harness/agents/oh-facade.md +75 -0
- package/harness/agents/oh-fusion.md +45 -0
- package/harness/agents/oh-gauntlet.md +71 -0
- package/harness/agents/oh-grill.md +71 -0
- package/harness/agents/oh-investigate.md +60 -0
- package/harness/agents/oh-manifest.md +95 -0
- package/harness/agents/oh-plan-review.md +40 -0
- package/harness/agents/oh-planner.md +50 -0
- package/harness/agents/oh-refactor.md +37 -0
- package/harness/agents/oh-retro.md +46 -0
- package/harness/agents/oh-review.md +85 -0
- package/harness/agents/oh-security.md +83 -0
- package/harness/agents/oh-ship.md +76 -0
- package/harness/agents/oh-skill-craft.md +38 -0
- package/harness/agents/openhermes.md +28 -73
- package/harness/codex/AUTOPILOT.md +235 -87
- package/harness/codex/CHARTER.md +80 -0
- package/harness/instructions/SHELL.md +76 -0
- package/harness/lib/background/background.test.ts +197 -0
- package/harness/lib/background/index.ts +7 -0
- package/harness/lib/background/interfaces.ts +31 -0
- package/harness/lib/background/manager.ts +320 -0
- package/harness/lib/composer/compose.test.ts +168 -0
- package/harness/lib/composer/compose.ts +65 -0
- package/harness/lib/composer/fragments/01-identity.md +1 -0
- package/harness/lib/composer/fragments/02-delegation.md +6 -0
- package/harness/lib/composer/fragments/03-permissions.md +13 -0
- package/harness/lib/composer/fragments/04-task-flow.md +15 -0
- package/harness/lib/composer/fragments/05-confidence.md +5 -0
- package/harness/lib/composer/fragments/06-parallelization.md +17 -0
- package/harness/lib/composer/fragments/07-shell.md +41 -0
- package/harness/lib/composer/fragments/08-routing.md +8 -0
- package/harness/lib/composer/fragments/09-guardrails.md +12 -0
- package/harness/lib/composer/index.ts +1 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +70 -0
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +59 -0
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +147 -0
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
- package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
- package/harness/lib/hooks/hooks.test.ts +1016 -0
- package/harness/lib/hooks/index.ts +30 -0
- package/harness/lib/hooks/registry.ts +416 -0
- package/harness/lib/hooks/types.ts +71 -0
- package/harness/lib/memory/index.ts +18 -0
- package/harness/lib/memory/interfaces.ts +53 -0
- package/harness/lib/memory/memory-manager.ts +205 -0
- package/harness/lib/memory/memory.test.ts +491 -0
- package/harness/lib/memory/plan-store.ts +366 -0
- package/harness/lib/recovery/handler.ts +243 -0
- package/harness/lib/recovery/index.ts +14 -0
- package/harness/lib/recovery/interfaces.ts +48 -0
- package/harness/lib/recovery/patterns.ts +149 -0
- package/harness/lib/recovery/recovery.test.ts +312 -0
- package/harness/lib/sanity/anomaly-tracker.ts +127 -0
- package/harness/lib/sanity/checker.ts +178 -0
- package/harness/lib/sanity/index.ts +13 -0
- package/harness/lib/sanity/interfaces.ts +24 -0
- package/harness/lib/sanity/sanity.test.ts +472 -0
- package/harness/lib/sync/file-watcher.ts +174 -0
- package/harness/lib/sync/index.ts +11 -0
- package/harness/lib/sync/interfaces.ts +27 -0
- package/harness/lib/sync/plan-sync.ts +536 -0
- package/harness/lib/sync/sync.test.ts +832 -0
- package/harness/skills/oh-ascii/DEEP.md +292 -0
- package/harness/skills/oh-ascii/SKILL.md +31 -0
- package/harness/skills/oh-ascii/scripts/check_ascii_alignment.py +596 -0
- package/harness/skills/oh-browser/DEEP.md +54 -0
- package/harness/skills/oh-browser/SKILL.md +30 -0
- package/harness/skills/oh-builder/DEEP.md +63 -0
- package/harness/skills/oh-builder/SKILL.md +12 -90
- package/harness/skills/oh-expert/DEEP.md +85 -0
- package/harness/skills/oh-expert/SKILL.md +13 -106
- package/harness/skills/oh-facade/DEEP.md +182 -0
- package/harness/skills/oh-facade/SKILL.md +15 -279
- package/harness/skills/oh-freeze/DEEP.md +18 -0
- package/harness/skills/oh-freeze/SKILL.md +10 -19
- package/harness/skills/oh-full-output/DEEP.md +25 -0
- package/harness/skills/oh-full-output/SKILL.md +12 -65
- package/harness/skills/oh-fusion/DEEP.md +120 -0
- package/harness/skills/oh-fusion/SKILL.md +17 -295
- package/harness/skills/oh-gauntlet/DEEP.md +77 -0
- package/harness/skills/oh-gauntlet/SKILL.md +13 -105
- package/harness/skills/oh-grill/DEEP.md +51 -0
- package/harness/skills/oh-grill/SKILL.md +12 -63
- package/harness/skills/oh-guard/DEEP.md +19 -0
- package/harness/skills/oh-guard/SKILL.md +10 -24
- package/harness/skills/oh-handoff/DEEP.md +48 -0
- package/harness/skills/oh-handoff/SKILL.md +13 -23
- package/harness/skills/oh-health/DEEP.md +74 -0
- package/harness/skills/oh-health/SKILL.md +13 -76
- package/harness/skills/oh-init/DEEP.md +85 -0
- package/harness/skills/oh-init/SKILL.md +13 -127
- package/harness/skills/oh-investigate/DEEP.md +171 -0
- package/harness/skills/oh-investigate/SKILL.md +13 -66
- package/harness/skills/oh-issue/DEEP.md +21 -0
- package/harness/skills/oh-issue/SKILL.md +11 -27
- package/harness/skills/oh-manifest/DEEP.md +92 -0
- package/harness/skills/oh-manifest/SKILL.md +12 -109
- package/harness/skills/oh-plan-review/DEEP.md +90 -0
- package/harness/skills/oh-plan-review/SKILL.md +13 -115
- package/harness/skills/oh-planner/DEEP.md +172 -0
- package/harness/skills/oh-planner/SKILL.md +12 -149
- package/harness/skills/oh-prd/DEEP.md +45 -0
- package/harness/skills/oh-prd/SKILL.md +10 -26
- package/harness/skills/oh-refactor/DEEP.md +122 -0
- package/harness/skills/oh-refactor/SKILL.md +17 -410
- package/harness/skills/oh-retro/DEEP.md +26 -0
- package/harness/skills/oh-retro/SKILL.md +12 -24
- package/harness/skills/oh-review/DEEP.md +87 -0
- package/harness/skills/oh-review/SKILL.md +11 -97
- package/harness/skills/oh-security/DEEP.md +83 -0
- package/harness/skills/oh-security/SKILL.md +14 -96
- package/harness/skills/oh-ship/DEEP.md +141 -0
- package/harness/skills/oh-ship/SKILL.md +14 -32
- package/harness/skills/oh-skill-craft/DEEP.md +369 -0
- package/harness/skills/oh-skill-craft/SKILL.md +13 -177
- package/harness/skills/oh-skills-link/DEEP.md +16 -0
- package/harness/skills/oh-skills-link/SKILL.md +10 -20
- package/harness/skills/oh-skills-list/DEEP.md +20 -0
- package/harness/skills/oh-skills-list/SKILL.md +9 -22
- package/harness/skills/oh-triage/DEEP.md +23 -0
- package/harness/skills/oh-triage/SKILL.md +8 -24
- package/harness/skills/oh-worktree/DEEP.md +169 -0
- package/harness/skills/oh-worktree/SKILL.md +32 -0
- package/lib/harness-resolver.ts +8 -10
- package/package.json +7 -5
- package/tsconfig.json +1 -1
- package/harness/codex/CONSTITUTION.md +0 -73
- package/harness/codex/ROUTING.md +0 -92
- package/harness/commands/oh-doctor.md +0 -26
- package/harness/commands/oh-log.md +0 -18
- package/harness/instructions/RUNTIME.md +0 -30
- package/harness/skills/oh-caveman/SKILL.md +0 -42
- package/harness/skills/oh-learn/SKILL.md +0 -101
- package/lib/logger.ts +0 -75
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""
|
|
6
|
+
ASCII Diagram Alignment Validator
|
|
7
|
+
|
|
8
|
+
Validates alignment of box-drawing characters in enclosed box diagrams.
|
|
9
|
+
Skips file tree structures (├── patterns) and handles arrow characters.
|
|
10
|
+
Outputs issues in compiler-like format: file:line:column: severity: message
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
uv run check_ascii_alignment.py <file_or_directory> [--warn-only] [--verbose]
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
uv run check_ascii_alignment.py docs/ARCHITECTURE.md
|
|
17
|
+
uv run check_ascii_alignment.py docs/*.md
|
|
18
|
+
uv run check_ascii_alignment.py docs/ --verbose
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Box-drawing character sets
|
|
28
|
+
# Light (single) lines
|
|
29
|
+
SINGLE_HORIZONTAL = set('─')
|
|
30
|
+
SINGLE_VERTICAL = set('│')
|
|
31
|
+
# Double lines
|
|
32
|
+
DOUBLE_HORIZONTAL = set('═')
|
|
33
|
+
DOUBLE_VERTICAL = set('║')
|
|
34
|
+
# Heavy (bold) lines - used by graph-easy boxart
|
|
35
|
+
HEAVY_HORIZONTAL = set('━')
|
|
36
|
+
HEAVY_VERTICAL = set('┃')
|
|
37
|
+
|
|
38
|
+
HORIZONTAL = SINGLE_HORIZONTAL | DOUBLE_HORIZONTAL | HEAVY_HORIZONTAL
|
|
39
|
+
VERTICAL = SINGLE_VERTICAL | DOUBLE_VERTICAL | HEAVY_VERTICAL
|
|
40
|
+
|
|
41
|
+
# Corners - Light
|
|
42
|
+
CORNER_TL_LIGHT = set('┌')
|
|
43
|
+
CORNER_TR_LIGHT = set('┐')
|
|
44
|
+
CORNER_BL_LIGHT = set('└')
|
|
45
|
+
CORNER_BR_LIGHT = set('┘')
|
|
46
|
+
# Corners - Double
|
|
47
|
+
CORNER_TL_DOUBLE = set('╔')
|
|
48
|
+
CORNER_TR_DOUBLE = set('╗')
|
|
49
|
+
CORNER_BL_DOUBLE = set('╚')
|
|
50
|
+
CORNER_BR_DOUBLE = set('╝')
|
|
51
|
+
# Corners - Heavy (bold) - used by graph-easy boxart
|
|
52
|
+
CORNER_TL_HEAVY = set('┏')
|
|
53
|
+
CORNER_TR_HEAVY = set('┓')
|
|
54
|
+
CORNER_BL_HEAVY = set('┗')
|
|
55
|
+
CORNER_BR_HEAVY = set('┛')
|
|
56
|
+
# Corners - Rounded (arc) - used by graph-easy shape: rounded
|
|
57
|
+
CORNER_TL_ROUNDED = set('╭')
|
|
58
|
+
CORNER_TR_ROUNDED = set('╮')
|
|
59
|
+
CORNER_BL_ROUNDED = set('╰')
|
|
60
|
+
CORNER_BR_ROUNDED = set('╯')
|
|
61
|
+
|
|
62
|
+
CORNER_TL = CORNER_TL_LIGHT | CORNER_TL_DOUBLE | CORNER_TL_HEAVY | CORNER_TL_ROUNDED
|
|
63
|
+
CORNER_TR = CORNER_TR_LIGHT | CORNER_TR_DOUBLE | CORNER_TR_HEAVY | CORNER_TR_ROUNDED
|
|
64
|
+
CORNER_BL = CORNER_BL_LIGHT | CORNER_BL_DOUBLE | CORNER_BL_HEAVY | CORNER_BL_ROUNDED
|
|
65
|
+
CORNER_BR = CORNER_BR_LIGHT | CORNER_BR_DOUBLE | CORNER_BR_HEAVY | CORNER_BR_ROUNDED
|
|
66
|
+
CORNERS = CORNER_TL | CORNER_TR | CORNER_BL | CORNER_BR
|
|
67
|
+
|
|
68
|
+
# T-junctions - Light
|
|
69
|
+
T_LEFT_LIGHT = set('├')
|
|
70
|
+
T_RIGHT_LIGHT = set('┤')
|
|
71
|
+
T_TOP_LIGHT = set('┬')
|
|
72
|
+
T_BOTTOM_LIGHT = set('┴')
|
|
73
|
+
# T-junctions - Double
|
|
74
|
+
T_LEFT_DOUBLE = set('╠╞╟')
|
|
75
|
+
T_RIGHT_DOUBLE = set('╣╡╢')
|
|
76
|
+
T_TOP_DOUBLE = set('╦╤╥')
|
|
77
|
+
T_BOTTOM_DOUBLE = set('╩╧╨')
|
|
78
|
+
# T-junctions - Heavy (bold) - used by graph-easy boxart
|
|
79
|
+
T_LEFT_HEAVY = set('┣┡┢┝┞┟┠')
|
|
80
|
+
T_RIGHT_HEAVY = set('┫┥┦┧┨┩┪')
|
|
81
|
+
T_TOP_HEAVY = set('┳┭┮┯┰┱┲')
|
|
82
|
+
T_BOTTOM_HEAVY = set('┻┵┶┷┸┹┺')
|
|
83
|
+
|
|
84
|
+
T_LEFT = T_LEFT_LIGHT | T_LEFT_DOUBLE | T_LEFT_HEAVY
|
|
85
|
+
T_RIGHT = T_RIGHT_LIGHT | T_RIGHT_DOUBLE | T_RIGHT_HEAVY
|
|
86
|
+
T_TOP = T_TOP_LIGHT | T_TOP_DOUBLE | T_TOP_HEAVY
|
|
87
|
+
T_BOTTOM = T_BOTTOM_LIGHT | T_BOTTOM_DOUBLE | T_BOTTOM_HEAVY
|
|
88
|
+
T_JUNCTIONS = T_LEFT | T_RIGHT | T_TOP | T_BOTTOM
|
|
89
|
+
|
|
90
|
+
# Crosses - Light, Double, Heavy
|
|
91
|
+
CROSSES = set('┼╬╪╫╋')
|
|
92
|
+
|
|
93
|
+
# Arrow characters (valid terminators for lines)
|
|
94
|
+
# Includes graph-easy arrows: ∨∧ (mathematical symbols used as arrows)
|
|
95
|
+
ARROWS = set('▶▷►▻▸▹→⟶⟹▼▽▾▿↓⇓◀◁◄◅◂◃←⟵⟸▲△▴▵↑⇑∨∧<>')
|
|
96
|
+
|
|
97
|
+
# Block elements - used by graph-easy as decorative borders (valid terminators)
|
|
98
|
+
# ▐ (right half block), ▌ (left half block), ▀ (upper half), ▄ (lower half)
|
|
99
|
+
BLOCK_ELEMENTS = set('▐▌▀▄█░▒▓')
|
|
100
|
+
|
|
101
|
+
# Ellipsis characters - used by graph-easy for truncation (valid terminators)
|
|
102
|
+
# ⋮ (vertical ellipsis), ⋯ (horizontal ellipsis), … (horizontal ellipsis)
|
|
103
|
+
ELLIPSIS_CHARS = set('⋮⋯…')
|
|
104
|
+
|
|
105
|
+
# Valid line terminators (arrows + block elements + ellipsis)
|
|
106
|
+
VALID_TERMINATORS = ARROWS | BLOCK_ELEMENTS | ELLIPSIS_CHARS
|
|
107
|
+
|
|
108
|
+
# All box-drawing characters
|
|
109
|
+
ALL_BOX_CHARS = HORIZONTAL | VERTICAL | CORNERS | T_JUNCTIONS | CROSSES
|
|
110
|
+
|
|
111
|
+
# Characters that connect upward
|
|
112
|
+
CONNECTS_UP = VERTICAL | CORNER_BL | CORNER_BR | T_LEFT | T_RIGHT | T_BOTTOM | CROSSES
|
|
113
|
+
# Characters that connect downward
|
|
114
|
+
CONNECTS_DOWN = VERTICAL | CORNER_TL | CORNER_TR | T_LEFT | T_RIGHT | T_TOP | CROSSES
|
|
115
|
+
# Characters that connect left
|
|
116
|
+
CONNECTS_LEFT = HORIZONTAL | CORNER_TR | CORNER_BR | T_TOP | T_BOTTOM | T_RIGHT | CROSSES
|
|
117
|
+
# Characters that connect right
|
|
118
|
+
CONNECTS_RIGHT = HORIZONTAL | CORNER_TL | CORNER_BL | T_TOP | T_BOTTOM | T_LEFT | CROSSES
|
|
119
|
+
|
|
120
|
+
# File tree patterns to skip (require space after dashes for actual tree patterns)
|
|
121
|
+
FILE_TREE_PATTERN = re.compile(r'[├└]── ')
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Severity(Enum):
|
|
125
|
+
ERROR = "error"
|
|
126
|
+
WARNING = "warning"
|
|
127
|
+
INFO = "info"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class Issue:
|
|
132
|
+
file: str
|
|
133
|
+
line: int
|
|
134
|
+
column: int
|
|
135
|
+
severity: Severity
|
|
136
|
+
message: str
|
|
137
|
+
suggestion: str | None = None
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
base = f"{self.file}:{self.line}:{self.column}: {self.severity.value}: {self.message}"
|
|
141
|
+
if self.suggestion:
|
|
142
|
+
base += f"\n → Suggestion: {self.suggestion}"
|
|
143
|
+
return base
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_file_tree_block(lines: list[str]) -> bool:
|
|
147
|
+
"""Detect if a code block is a file tree structure."""
|
|
148
|
+
tree_line_count = 0
|
|
149
|
+
for line in lines:
|
|
150
|
+
if FILE_TREE_PATTERN.search(line):
|
|
151
|
+
tree_line_count += 1
|
|
152
|
+
# If more than 20% of lines have tree patterns, it's a file tree
|
|
153
|
+
return tree_line_count > len(lines) * 0.2
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_enclosed_box_diagram(lines: list[str]) -> bool:
|
|
157
|
+
"""Detect if a code block contains an enclosed box diagram (with corner characters)."""
|
|
158
|
+
has_tl = any(any(c in CORNER_TL for c in line) for line in lines)
|
|
159
|
+
has_tr = any(any(c in CORNER_TR for c in line) for line in lines)
|
|
160
|
+
has_bl = any(any(c in CORNER_BL for c in line) for line in lines)
|
|
161
|
+
has_br = any(any(c in CORNER_BR for c in line) for line in lines)
|
|
162
|
+
|
|
163
|
+
# For a proper enclosed box, we need at least top-left + bottom-right or top-right + bottom-left
|
|
164
|
+
return (has_tl and has_br) or (has_tr and has_bl) or (has_tl and has_tr and has_bl and has_br)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_char_at(lines: list[str], row: int, col: int) -> str | None:
|
|
168
|
+
"""Get character at position, handling bounds."""
|
|
169
|
+
if row < 0 or row >= len(lines):
|
|
170
|
+
return None
|
|
171
|
+
line = lines[row]
|
|
172
|
+
if col < 0 or col >= len(line):
|
|
173
|
+
return None
|
|
174
|
+
return line[col]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def find_box_chars_in_line(line: str) -> list[tuple[int, str]]:
|
|
178
|
+
"""Find all box-drawing characters in a line with their column positions."""
|
|
179
|
+
results = []
|
|
180
|
+
for col, char in enumerate(line):
|
|
181
|
+
if char in ALL_BOX_CHARS:
|
|
182
|
+
results.append((col, char))
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_vertical_alignment(
|
|
187
|
+
lines: list[str], row: int, col: int, char: str, file: str
|
|
188
|
+
) -> list[Issue]:
|
|
189
|
+
"""Check vertical alignment for a character that connects up or down."""
|
|
190
|
+
issues = []
|
|
191
|
+
|
|
192
|
+
# Check if character should connect upward
|
|
193
|
+
if char in CONNECTS_UP:
|
|
194
|
+
above = get_char_at(lines, row - 1, col)
|
|
195
|
+
# Valid connections: box chars that connect down, arrows, terminators,
|
|
196
|
+
# or horizontal lines (graph-easy arrow stem pattern: │ below ─)
|
|
197
|
+
if above is not None and above not in CONNECTS_DOWN and above not in ' \t' and above not in VALID_TERMINATORS and above not in HORIZONTAL:
|
|
198
|
+
issues.append(Issue(
|
|
199
|
+
file=file,
|
|
200
|
+
line=row + 1, # 1-indexed
|
|
201
|
+
column=col + 1,
|
|
202
|
+
severity=Severity.ERROR,
|
|
203
|
+
message=f"vertical connector '{char}' at column {col + 1} has no matching character above (found '{above}')",
|
|
204
|
+
suggestion=f"Add '│', '├', '┤', '┬', or '┼' at line {row}, column {col + 1}, or check if '{char}' should be a different character"
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
# Check if character should connect downward
|
|
208
|
+
if char in CONNECTS_DOWN:
|
|
209
|
+
below = get_char_at(lines, row + 1, col)
|
|
210
|
+
# Valid connections: box chars that connect up, arrows, terminators,
|
|
211
|
+
# or horizontal lines (graph-easy arrow stem pattern: │ above ─)
|
|
212
|
+
if below is not None and below not in CONNECTS_UP and below not in ' \t' and below not in VALID_TERMINATORS and below not in HORIZONTAL:
|
|
213
|
+
issues.append(Issue(
|
|
214
|
+
file=file,
|
|
215
|
+
line=row + 1,
|
|
216
|
+
column=col + 1,
|
|
217
|
+
severity=Severity.ERROR,
|
|
218
|
+
message=f"vertical connector '{char}' at column {col + 1} has no matching character below (found '{below}')",
|
|
219
|
+
suggestion=f"Add '│', '├', '┤', '┴', or '┼' at line {row + 2}, column {col + 1}, or check if '{char}' should be a different character"
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
return issues
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def check_horizontal_alignment(
|
|
226
|
+
lines: list[str], row: int, col: int, char: str, file: str
|
|
227
|
+
) -> list[Issue]:
|
|
228
|
+
"""Check horizontal alignment for a character that connects left or right."""
|
|
229
|
+
issues = []
|
|
230
|
+
line = lines[row]
|
|
231
|
+
|
|
232
|
+
# Check if character should connect left
|
|
233
|
+
if char in CONNECTS_LEFT:
|
|
234
|
+
left = get_char_at(lines, row, col - 1)
|
|
235
|
+
if col > 0 and left is not None and left not in CONNECTS_RIGHT and left not in ' \t' and left not in VALID_TERMINATORS:
|
|
236
|
+
issues.append(Issue(
|
|
237
|
+
file=file,
|
|
238
|
+
line=row + 1,
|
|
239
|
+
column=col + 1,
|
|
240
|
+
severity=Severity.ERROR,
|
|
241
|
+
message=f"horizontal connector '{char}' has no matching character to the left (found '{left}')",
|
|
242
|
+
suggestion=f"Add '─', '┌', '└', '┬', '┴', or '┼' at column {col}"
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
# Check if character should connect right
|
|
246
|
+
if char in CONNECTS_RIGHT:
|
|
247
|
+
right = get_char_at(lines, row, col + 1)
|
|
248
|
+
if col < len(line) - 1 and right is not None and right not in CONNECTS_LEFT and right not in ' \t' and right not in VALID_TERMINATORS:
|
|
249
|
+
issues.append(Issue(
|
|
250
|
+
file=file,
|
|
251
|
+
line=row + 1,
|
|
252
|
+
column=col + 1,
|
|
253
|
+
severity=Severity.ERROR,
|
|
254
|
+
message=f"horizontal connector '{char}' has no matching character to the right (found '{right}')",
|
|
255
|
+
suggestion=f"Add '─', '┐', '┘', '┬', '┴', or '┼' at column {col + 2}"
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
return issues
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def check_corner_connections(
|
|
262
|
+
lines: list[str], row: int, col: int, char: str, file: str
|
|
263
|
+
) -> list[Issue]:
|
|
264
|
+
"""Validate corner characters have proper connections."""
|
|
265
|
+
issues = []
|
|
266
|
+
|
|
267
|
+
# Top-left corner: should connect right and down
|
|
268
|
+
if char in CORNER_TL:
|
|
269
|
+
right = get_char_at(lines, row, col + 1)
|
|
270
|
+
below = get_char_at(lines, row + 1, col)
|
|
271
|
+
|
|
272
|
+
if right is not None and right not in CONNECTS_LEFT and right not in ' \t\n' and right not in VALID_TERMINATORS:
|
|
273
|
+
issues.append(Issue(
|
|
274
|
+
file=file,
|
|
275
|
+
line=row + 1,
|
|
276
|
+
column=col + 1,
|
|
277
|
+
severity=Severity.ERROR,
|
|
278
|
+
message=f"top-left corner '{char}' not connected to the right",
|
|
279
|
+
suggestion="Add horizontal line '─' or '═' after the corner"
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
if below is not None and below not in CONNECTS_UP and below not in ' \t\n' and below not in VALID_TERMINATORS:
|
|
283
|
+
issues.append(Issue(
|
|
284
|
+
file=file,
|
|
285
|
+
line=row + 1,
|
|
286
|
+
column=col + 1,
|
|
287
|
+
severity=Severity.ERROR,
|
|
288
|
+
message=f"top-left corner '{char}' not connected below",
|
|
289
|
+
suggestion=f"Add vertical line '│' or '║' at line {row + 2}, column {col + 1}"
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
# Top-right corner: should connect left and down
|
|
293
|
+
if char in CORNER_TR:
|
|
294
|
+
left = get_char_at(lines, row, col - 1)
|
|
295
|
+
below = get_char_at(lines, row + 1, col)
|
|
296
|
+
|
|
297
|
+
if left is not None and left not in CONNECTS_RIGHT and left not in ' \t\n' and left not in VALID_TERMINATORS:
|
|
298
|
+
issues.append(Issue(
|
|
299
|
+
file=file,
|
|
300
|
+
line=row + 1,
|
|
301
|
+
column=col + 1,
|
|
302
|
+
severity=Severity.ERROR,
|
|
303
|
+
message=f"top-right corner '{char}' not connected to the left",
|
|
304
|
+
suggestion="Add horizontal line '─' or '═' before the corner"
|
|
305
|
+
))
|
|
306
|
+
|
|
307
|
+
if below is not None and below not in CONNECTS_UP and below not in ' \t\n' and below not in VALID_TERMINATORS:
|
|
308
|
+
issues.append(Issue(
|
|
309
|
+
file=file,
|
|
310
|
+
line=row + 1,
|
|
311
|
+
column=col + 1,
|
|
312
|
+
severity=Severity.ERROR,
|
|
313
|
+
message=f"top-right corner '{char}' not connected below",
|
|
314
|
+
suggestion=f"Add vertical line '│' or '║' at line {row + 2}, column {col + 1}"
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
# Bottom-left corner: should connect right and up
|
|
318
|
+
if char in CORNER_BL:
|
|
319
|
+
right = get_char_at(lines, row, col + 1)
|
|
320
|
+
above = get_char_at(lines, row - 1, col)
|
|
321
|
+
|
|
322
|
+
if right is not None and right not in CONNECTS_LEFT and right not in ' \t\n' and right not in VALID_TERMINATORS:
|
|
323
|
+
issues.append(Issue(
|
|
324
|
+
file=file,
|
|
325
|
+
line=row + 1,
|
|
326
|
+
column=col + 1,
|
|
327
|
+
severity=Severity.ERROR,
|
|
328
|
+
message=f"bottom-left corner '{char}' not connected to the right",
|
|
329
|
+
suggestion="Add horizontal line '─' or '═' after the corner"
|
|
330
|
+
))
|
|
331
|
+
|
|
332
|
+
if above is not None and above not in CONNECTS_DOWN and above not in ' \t\n' and above not in VALID_TERMINATORS:
|
|
333
|
+
issues.append(Issue(
|
|
334
|
+
file=file,
|
|
335
|
+
line=row + 1,
|
|
336
|
+
column=col + 1,
|
|
337
|
+
severity=Severity.ERROR,
|
|
338
|
+
message=f"bottom-left corner '{char}' not connected above",
|
|
339
|
+
suggestion=f"Add vertical line '│' or '║' at line {row}, column {col + 1}"
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
# Bottom-right corner: should connect left and up
|
|
343
|
+
if char in CORNER_BR:
|
|
344
|
+
left = get_char_at(lines, row, col - 1)
|
|
345
|
+
above = get_char_at(lines, row - 1, col)
|
|
346
|
+
|
|
347
|
+
if left is not None and left not in CONNECTS_RIGHT and left not in ' \t\n' and left not in VALID_TERMINATORS:
|
|
348
|
+
issues.append(Issue(
|
|
349
|
+
file=file,
|
|
350
|
+
line=row + 1,
|
|
351
|
+
column=col + 1,
|
|
352
|
+
severity=Severity.ERROR,
|
|
353
|
+
message=f"bottom-right corner '{char}' not connected to the left",
|
|
354
|
+
suggestion="Add horizontal line '─' or '═' before the corner"
|
|
355
|
+
))
|
|
356
|
+
|
|
357
|
+
if above is not None and above not in CONNECTS_DOWN and above not in ' \t\n' and above not in VALID_TERMINATORS:
|
|
358
|
+
issues.append(Issue(
|
|
359
|
+
file=file,
|
|
360
|
+
line=row + 1,
|
|
361
|
+
column=col + 1,
|
|
362
|
+
severity=Severity.ERROR,
|
|
363
|
+
message=f"bottom-right corner '{char}' not connected above",
|
|
364
|
+
suggestion=f"Add vertical line '│' or '║' at line {row}, column {col + 1}"
|
|
365
|
+
))
|
|
366
|
+
|
|
367
|
+
return issues
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def extract_code_blocks(content: str) -> list[tuple[int, list[str]]]:
|
|
371
|
+
"""Extract code blocks from markdown, returning (start_line, lines) tuples."""
|
|
372
|
+
blocks = []
|
|
373
|
+
lines = content.split('\n')
|
|
374
|
+
in_block = False
|
|
375
|
+
block_start = 0
|
|
376
|
+
block_lines = []
|
|
377
|
+
|
|
378
|
+
for i, line in enumerate(lines):
|
|
379
|
+
stripped = line.strip()
|
|
380
|
+
if stripped.startswith('```'):
|
|
381
|
+
if in_block:
|
|
382
|
+
# End of block
|
|
383
|
+
blocks.append((block_start, block_lines))
|
|
384
|
+
block_lines = []
|
|
385
|
+
in_block = False
|
|
386
|
+
else:
|
|
387
|
+
# Start of block
|
|
388
|
+
in_block = True
|
|
389
|
+
block_start = i + 1 # Next line is start of content
|
|
390
|
+
elif in_block:
|
|
391
|
+
block_lines.append(line)
|
|
392
|
+
|
|
393
|
+
return blocks
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def validate_block(block_lines: list[str], block_start: int, file_path: str, verbose: bool) -> list[Issue]:
|
|
397
|
+
"""Validate a single code block for ASCII diagram alignment issues."""
|
|
398
|
+
issues = []
|
|
399
|
+
|
|
400
|
+
# Check if this block contains any box-drawing characters
|
|
401
|
+
has_box_chars = any(
|
|
402
|
+
any(c in ALL_BOX_CHARS for c in line)
|
|
403
|
+
for line in block_lines
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if not has_box_chars:
|
|
407
|
+
return issues
|
|
408
|
+
|
|
409
|
+
# Skip file tree structures
|
|
410
|
+
if is_file_tree_block(block_lines):
|
|
411
|
+
if verbose:
|
|
412
|
+
print(f" Skipping file tree structure at line {block_start + 1}")
|
|
413
|
+
return issues
|
|
414
|
+
|
|
415
|
+
# Only validate enclosed box diagrams
|
|
416
|
+
if not is_enclosed_box_diagram(block_lines):
|
|
417
|
+
if verbose:
|
|
418
|
+
print(f" Skipping non-enclosed diagram at line {block_start + 1}")
|
|
419
|
+
return issues
|
|
420
|
+
|
|
421
|
+
if verbose:
|
|
422
|
+
print(f" Checking enclosed box diagram at line {block_start + 1}")
|
|
423
|
+
|
|
424
|
+
# Validate each line in the block
|
|
425
|
+
for rel_row, line in enumerate(block_lines):
|
|
426
|
+
box_chars = find_box_chars_in_line(line)
|
|
427
|
+
|
|
428
|
+
for col, char in box_chars:
|
|
429
|
+
# Check vertical alignment
|
|
430
|
+
vert_issues = check_vertical_alignment(
|
|
431
|
+
block_lines, rel_row, col, char, file_path
|
|
432
|
+
)
|
|
433
|
+
# Adjust line numbers to absolute file positions
|
|
434
|
+
for issue in vert_issues:
|
|
435
|
+
issues.append(Issue(
|
|
436
|
+
file=issue.file,
|
|
437
|
+
line=block_start + issue.line,
|
|
438
|
+
column=issue.column,
|
|
439
|
+
severity=issue.severity,
|
|
440
|
+
message=issue.message,
|
|
441
|
+
suggestion=issue.suggestion
|
|
442
|
+
))
|
|
443
|
+
|
|
444
|
+
# Check horizontal alignment
|
|
445
|
+
horiz_issues = check_horizontal_alignment(
|
|
446
|
+
block_lines, rel_row, col, char, file_path
|
|
447
|
+
)
|
|
448
|
+
for issue in horiz_issues:
|
|
449
|
+
issues.append(Issue(
|
|
450
|
+
file=issue.file,
|
|
451
|
+
line=block_start + issue.line,
|
|
452
|
+
column=issue.column,
|
|
453
|
+
severity=issue.severity,
|
|
454
|
+
message=issue.message,
|
|
455
|
+
suggestion=issue.suggestion
|
|
456
|
+
))
|
|
457
|
+
|
|
458
|
+
# Check corner connections
|
|
459
|
+
if char in CORNERS:
|
|
460
|
+
corner_issues = check_corner_connections(
|
|
461
|
+
block_lines, rel_row, col, char, file_path
|
|
462
|
+
)
|
|
463
|
+
for issue in corner_issues:
|
|
464
|
+
issues.append(Issue(
|
|
465
|
+
file=issue.file,
|
|
466
|
+
line=block_start + issue.line,
|
|
467
|
+
column=issue.column,
|
|
468
|
+
severity=issue.severity,
|
|
469
|
+
message=issue.message,
|
|
470
|
+
suggestion=issue.suggestion
|
|
471
|
+
))
|
|
472
|
+
|
|
473
|
+
return issues
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def validate_file(file_path: Path, verbose: bool = False) -> list[Issue]:
|
|
477
|
+
"""Validate a single file for ASCII diagram alignment issues."""
|
|
478
|
+
issues = []
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
content = file_path.read_text(encoding='utf-8')
|
|
482
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
483
|
+
issues.append(Issue(
|
|
484
|
+
file=str(file_path),
|
|
485
|
+
line=0,
|
|
486
|
+
column=0,
|
|
487
|
+
severity=Severity.ERROR,
|
|
488
|
+
message=f"Could not read file: {e}"
|
|
489
|
+
))
|
|
490
|
+
return issues
|
|
491
|
+
|
|
492
|
+
# Extract code blocks from markdown
|
|
493
|
+
code_blocks = extract_code_blocks(content)
|
|
494
|
+
|
|
495
|
+
if verbose:
|
|
496
|
+
print(f" Found {len(code_blocks)} code block(s) in {file_path}")
|
|
497
|
+
|
|
498
|
+
for block_start, block_lines in code_blocks:
|
|
499
|
+
block_issues = validate_block(block_lines, block_start, str(file_path), verbose)
|
|
500
|
+
issues.extend(block_issues)
|
|
501
|
+
|
|
502
|
+
return issues
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def validate_path(path: Path, verbose: bool = False) -> list[Issue]:
|
|
506
|
+
"""Validate a file or directory."""
|
|
507
|
+
issues = []
|
|
508
|
+
|
|
509
|
+
if path.is_file():
|
|
510
|
+
if verbose:
|
|
511
|
+
print(f"Validating: {path}")
|
|
512
|
+
issues.extend(validate_file(path, verbose))
|
|
513
|
+
elif path.is_dir():
|
|
514
|
+
# Find all markdown files
|
|
515
|
+
for md_file in sorted(path.rglob('*.md')):
|
|
516
|
+
if verbose:
|
|
517
|
+
print(f"Validating: {md_file}")
|
|
518
|
+
issues.extend(validate_file(md_file, verbose))
|
|
519
|
+
else:
|
|
520
|
+
issues.append(Issue(
|
|
521
|
+
file=str(path),
|
|
522
|
+
line=0,
|
|
523
|
+
column=0,
|
|
524
|
+
severity=Severity.ERROR,
|
|
525
|
+
message=f"Path does not exist: {path}"
|
|
526
|
+
))
|
|
527
|
+
|
|
528
|
+
return issues
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def main():
|
|
532
|
+
parser = argparse.ArgumentParser(
|
|
533
|
+
description='Validate ASCII box-drawing diagram alignment in markdown files'
|
|
534
|
+
)
|
|
535
|
+
parser.add_argument(
|
|
536
|
+
'paths',
|
|
537
|
+
nargs='+',
|
|
538
|
+
type=Path,
|
|
539
|
+
help='Files or directories to validate'
|
|
540
|
+
)
|
|
541
|
+
parser.add_argument(
|
|
542
|
+
'--warn-only',
|
|
543
|
+
action='store_true',
|
|
544
|
+
help='Exit 0 even if warnings found (still exit 1 on errors)'
|
|
545
|
+
)
|
|
546
|
+
parser.add_argument(
|
|
547
|
+
'--verbose', '-v',
|
|
548
|
+
action='store_true',
|
|
549
|
+
help='Print verbose progress information'
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
args = parser.parse_args()
|
|
553
|
+
|
|
554
|
+
all_issues: list[Issue] = []
|
|
555
|
+
|
|
556
|
+
for path in args.paths:
|
|
557
|
+
all_issues.extend(validate_path(path, args.verbose))
|
|
558
|
+
|
|
559
|
+
# Deduplicate issues (same file:line:column:message)
|
|
560
|
+
seen = set()
|
|
561
|
+
unique_issues = []
|
|
562
|
+
for issue in all_issues:
|
|
563
|
+
key = (issue.file, issue.line, issue.column, issue.message)
|
|
564
|
+
if key not in seen:
|
|
565
|
+
seen.add(key)
|
|
566
|
+
unique_issues.append(issue)
|
|
567
|
+
|
|
568
|
+
# Sort by file, then line, then column
|
|
569
|
+
unique_issues.sort(key=lambda i: (i.file, i.line, i.column))
|
|
570
|
+
|
|
571
|
+
# Print issues
|
|
572
|
+
for issue in unique_issues:
|
|
573
|
+
print(issue)
|
|
574
|
+
|
|
575
|
+
# Summary
|
|
576
|
+
error_count = sum(1 for i in unique_issues if i.severity == Severity.ERROR)
|
|
577
|
+
warning_count = sum(1 for i in unique_issues if i.severity == Severity.WARNING)
|
|
578
|
+
info_count = sum(1 for i in unique_issues if i.severity == Severity.INFO)
|
|
579
|
+
|
|
580
|
+
if unique_issues:
|
|
581
|
+
print(f"\n{'─' * 60}")
|
|
582
|
+
print(f"Summary: {error_count} error(s), {warning_count} warning(s), {info_count} info")
|
|
583
|
+
|
|
584
|
+
# Exit code
|
|
585
|
+
if error_count > 0:
|
|
586
|
+
sys.exit(1)
|
|
587
|
+
elif warning_count > 0 and not args.warn_only:
|
|
588
|
+
sys.exit(2)
|
|
589
|
+
else:
|
|
590
|
+
if not unique_issues:
|
|
591
|
+
print("✓ No alignment issues found")
|
|
592
|
+
sys.exit(0)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
if __name__ == '__main__':
|
|
596
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# oh-browser — Deep Reference
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
User needs to interact with websites, automate browser tasks, scrape data, or test web applications. Triggers include: "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data", "test this web app", "login to a site", "automate browser", "browser automation", "web scraping", "check slack".
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- agent-browser installed globally: `npm install -g agent-browser && agent-browser install`
|
|
10
|
+
- Chrome/Chromium (auto-downloaded by `agent-browser install`)
|
|
11
|
+
- State files contain session tokens — add to `.gitignore`, never commit
|
|
12
|
+
|
|
13
|
+
## Common Patterns
|
|
14
|
+
|
|
15
|
+
- **Annotated screenshots**: `agent-browser screenshot --annotate` — overlays numbered labels matching `@eN` refs. Most reliable method for visual QA and multimodal AI workflows.
|
|
16
|
+
- **Batch execution**: `agent-browser batch "open url" "snapshot" "click @e1"` — avoids per-command startup overhead for multi-step workflows.
|
|
17
|
+
- **Session persistence**: `--session-name <name>` — auto-saves/restores cookies and localStorage. Login once, reuse across sessions.
|
|
18
|
+
- **Auth vault**: `agent-browser auth save <name> --url <url> --username <user>` — stores encrypted credentials. LLM never sees passwords.
|
|
19
|
+
- **Diff**: `agent-browser diff snapshot` — compare current vs last snapshot for change detection. `agent-browser diff screenshot --baseline before.png` for visual pixel diff.
|
|
20
|
+
- **Chrome profile reuse**: `--profile Default` — use existing Chrome login state with zero setup.
|
|
21
|
+
- **Tab labeling**: `agent-browser tab new --label docs <url>` — memorable labels, not numeric indices.
|
|
22
|
+
- **Parallel scrape**: Use `batch --json` with piped command arrays for structured multi-page workflows.
|
|
23
|
+
|
|
24
|
+
## Common Commands Reference
|
|
25
|
+
|
|
26
|
+
| Task | Command |
|
|
27
|
+
|------|---------|
|
|
28
|
+
| Open URL | `agent-browser open <url>` |
|
|
29
|
+
| Get page state | `agent-browser snapshot -i` (interactive only) |
|
|
30
|
+
| Click | `agent-browser click @eN` or `agent-browser click "css-selector"` |
|
|
31
|
+
| Type text | `agent-browser fill @eN "text"` |
|
|
32
|
+
| Screenshot | `agent-browser screenshot --annotate` |
|
|
33
|
+
| Extract text | `agent-browser get text @eN` |
|
|
34
|
+
| Run JS | `agent-browser eval "document.title"` |
|
|
35
|
+
| Wait for element | `agent-browser wait ".selector"` |
|
|
36
|
+
| Scroll | `agent-browser scroll down 200` |
|
|
37
|
+
| Multi-step | `agent-browser batch "cmd1" "cmd2" "cmd3"` |
|
|
38
|
+
|
|
39
|
+
## Anti-patterns
|
|
40
|
+
|
|
41
|
+
- Forgetting `agent-browser install` first (Chrome won't be available)
|
|
42
|
+
- Not closing browser sessions (daemon processes leak)
|
|
43
|
+
- Using CSS selectors when `@eN` refs from snapshot are faster and more reliable
|
|
44
|
+
- Running individual commands instead of batch for multi-step workflows
|
|
45
|
+
- Passing credentials in prompts instead of using auth vault
|
|
46
|
+
- Committing state files with session tokens to git
|
|
47
|
+
|
|
48
|
+
## Security
|
|
49
|
+
|
|
50
|
+
- Use `--allowed-domains "example.com"` to restrict navigation to trusted domains
|
|
51
|
+
- Use auth vault instead of passing credentials in LLM prompts
|
|
52
|
+
- Session state files contain tokens — keep in `.gitignore`
|
|
53
|
+
- `--action-policy ./policy.json` gates destructive actions
|
|
54
|
+
- `--content-boundaries` wraps page output in delimiters to distinguish tool output from page content
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oh-browser
|
|
3
|
+
description: "Browser automation via agent-browser CLI for web interaction and data extraction"
|
|
4
|
+
tier: 3
|
|
5
|
+
route:
|
|
6
|
+
pass: surface
|
|
7
|
+
fail: oh-browser
|
|
8
|
+
blocker: surface
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# oh-browser
|
|
12
|
+
|
|
13
|
+
Browser automation via agent-browser CLI. Navigate pages, fill forms, take screenshots, extract data.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
1. Install agent-browser and Chrome — `npm install -g agent-browser && agent-browser install`
|
|
18
|
+
2. Launch browser — `agent-browser open <url>` or `agent-browser open` then navigate
|
|
19
|
+
3. Snapshot page state — `agent-browser snapshot` returns accessibility tree with `@eN` refs
|
|
20
|
+
4. Interact using `@eN` refs — `agent-browser click/fill/select/hover @eN`
|
|
21
|
+
5. Extract data — `agent-browser get text/html @eN` or `agent-browser screenshot`
|
|
22
|
+
6. Close browser — `agent-browser close`
|
|
23
|
+
|
|
24
|
+
## Routing
|
|
25
|
+
|
|
26
|
+
| Outcome | Route |
|
|
27
|
+
|---------|-------|
|
|
28
|
+
| pass | → surface |
|
|
29
|
+
| fail | → oh-browser |
|
|
30
|
+
| blocker | → surface |
|