oh-my-opencode 4.19.4 → 5.0.0-beta.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 (374) hide show
  1. package/.agents/command/publish.md +44 -16
  2. package/.agents/skills/publish/SKILL.md +44 -16
  3. package/.agents/skills/work-with-pr/SKILL.md +37 -23
  4. package/.opencode/command/publish.md +44 -16
  5. package/.opencode/skills/work-with-pr/SKILL.md +37 -23
  6. package/README.ja.md +1 -1
  7. package/README.ko.md +1 -1
  8. package/README.md +18 -6
  9. package/README.ru.md +1 -1
  10. package/README.zh-cn.md +1 -1
  11. package/bin/oh-my-opencode.js +14 -1
  12. package/bin/oh-my-opencode.test.ts +21 -0
  13. package/dist/agents/atlas/agent.d.ts +0 -1
  14. package/dist/agents/sisyphus/grok-4.d.ts +20 -0
  15. package/dist/agents/sisyphus/index.d.ts +2 -0
  16. package/dist/agents/sisyphus-agent-config.d.ts +6 -0
  17. package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
  18. package/dist/agents/sisyphus-runtime-prompt-reconciler.d.ts +15 -4
  19. package/dist/agents/types.d.ts +2 -2
  20. package/dist/cli/index.js +1888 -807
  21. package/dist/cli/run/on-complete-hook.d.ts +2 -0
  22. package/dist/cli-node/index.js +1888 -807
  23. package/dist/config/schema/agent-overrides.d.ts +528 -0
  24. package/dist/config/schema/oh-my-opencode-config.d.ts +495 -0
  25. package/dist/features/monitor/batcher.d.ts +3 -1
  26. package/dist/features/monitor/manager-internals.d.ts +1 -0
  27. package/dist/features/monitor/output-injector-types.d.ts +2 -0
  28. package/dist/features/monitor/output-injector.d.ts +6 -0
  29. package/dist/hooks/atlas/final-wave-approval-gate.test-support.d.ts +50 -0
  30. package/dist/hooks/atlas/system-reminder-templates.d.ts +0 -1
  31. package/dist/hooks/todo-continuation-enforcer/types.d.ts +1 -0
  32. package/dist/hooks/todo-continuation-enforcer/unrecoverable-request-error.d.ts +9 -0
  33. package/dist/hooks/tool-pair-validator/hook.test-support.d.ts +29 -0
  34. package/dist/hooks/tool-pair-validator/tool-part-ids.d.ts +14 -5
  35. package/dist/hooks/tool-pair-validator/tool-result-repair.d.ts +4 -3
  36. package/dist/hooks/tool-pair-validator/types.d.ts +5 -22
  37. package/dist/index.js +3139 -2232
  38. package/dist/mcp/lsp.d.ts +1 -0
  39. package/dist/oh-my-opencode.schema.json +1466 -131
  40. package/dist/shared/normalize-sdk-response.d.ts +1 -0
  41. package/dist/shared/shell-env.d.ts +1 -1
  42. package/dist/shared/tmux/constants.d.ts +1 -1
  43. package/dist/skills/ast-grep/SOURCE +1 -1
  44. package/dist/skills/ast-grep/install.ps1 +2 -2
  45. package/dist/skills/ast-grep/install.sh +1 -1
  46. package/dist/skills/ast-grep/references/install.md +2 -2
  47. package/dist/skills/ast-grep/tests/smoke.sh +1 -1
  48. package/dist/skills/coding-agent-sessions/SKILL.md +3 -2
  49. package/dist/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  50. package/dist/skills/coding-agent-sessions/references/senpi.md +4 -4
  51. package/dist/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
  52. package/dist/skills/frontend/SKILL.md +10 -7
  53. package/dist/skills/frontend/references/design/_INDEX.md +1 -0
  54. package/dist/skills/frontend/references/design/stylegallery.md +80 -0
  55. package/dist/skills/start-work/SKILL.md +54 -9
  56. package/dist/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
  57. package/dist/skills/ultimate-browsing/SKILL.md +2 -2
  58. package/dist/skills/ultimate-browsing/engine/AGENTS.md +179 -0
  59. package/dist/skills/ultimate-browsing/engine/__main__.py +8 -1
  60. package/dist/skills/ultimate-browsing/engine/bias_check.py +11 -0
  61. package/dist/skills/ultimate-browsing/engine/fetch_chain.py +90 -52
  62. package/dist/skills/ultimate-browsing/engine/result_schema.py +10 -1
  63. package/dist/skills/ultimate-browsing/engine/surrogate.py +214 -0
  64. package/dist/skills/ultimate-browsing/engine/surrogates.yaml +60 -0
  65. package/dist/skills/ultimate-browsing/engine/templates/package.json +1 -1
  66. package/dist/skills/ultimate-browsing/engine/tests/fixtures/amp_redirect_stub.html +7 -0
  67. package/dist/skills/ultimate-browsing/engine/tests/fixtures/search_interstitial.html +19 -0
  68. package/dist/skills/ultimate-browsing/engine/tests/fixtures/wayback_available.json +1 -0
  69. package/dist/skills/ultimate-browsing/engine/tests/fixtures/wayback_snapshot.html +1128 -0
  70. package/dist/skills/ultimate-browsing/engine/tests/test_surrogate.py +252 -0
  71. package/dist/skills/ultimate-browsing/engine/tests/test_surrogate_validators.py +78 -0
  72. package/dist/skills/ultimate-browsing/engine/validators.py +46 -0
  73. package/dist/skills/ultimate-browsing/engine/waf_detector.py +1 -1
  74. package/dist/skills/ultimate-browsing/engine/waf_profiles.yaml +10 -5
  75. package/dist/skills/ultimate-browsing/references/agent-reach/social.md +1 -1
  76. package/dist/skills/ultimate-browsing/references/chrome-stealth.md +13 -11
  77. package/dist/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  78. package/dist/skills/ultimate-browsing/references/insane-search/cache-archive.md +51 -50
  79. package/dist/skills/ultimate-browsing/references/insane-search/fallback.md +1 -1
  80. package/dist/skills/ultimate-browsing/references/insane-search/jina.md +8 -2
  81. package/dist/skills/ultimate-browsing/references/insane-search/naver.md +1 -1
  82. package/dist/skills/ultimate-browsing/references/insane-search/twitter.md +3 -3
  83. package/dist/skills/ulw-plan/SKILL.md +3 -3
  84. package/dist/skills/ulw-plan/references/full-workflow.md +30 -6
  85. package/dist/skills/ulw-plan/references/intent-clear.md +2 -1
  86. package/dist/skills/ulw-plan/references/intent-unclear.md +3 -3
  87. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  88. package/dist/skills/ulw-research/SKILL.md +15 -10
  89. package/dist/tui.js +477 -36
  90. package/docs/reference/web-terminal-visual-qa.md +1 -1
  91. package/package.json +34 -24
  92. package/packages/lsp-core/src/lsp/client-diagnostics-concurrency.integration.test.ts +44 -0
  93. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +0 -28
  94. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +60 -7
  95. package/packages/lsp-core/src/lsp/client-wrapper.ts +69 -16
  96. package/packages/lsp-core/src/lsp/connection.ts +1 -1
  97. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +20 -1
  98. package/packages/lsp-core/src/tools/diagnostics.ts +3 -3
  99. package/packages/lsp-core/src/tools/navigation.ts +4 -2
  100. package/packages/lsp-core/src/tools/rename.ts +4 -2
  101. package/packages/lsp-core/src/tools/symbols.ts +1 -1
  102. package/packages/lsp-daemon/dist/cli.js +108 -40
  103. package/packages/lsp-daemon/dist/client.js +123 -55
  104. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
  105. package/packages/lsp-daemon/dist/ensure-daemon.js +18 -5
  106. package/packages/lsp-daemon/dist/index.js +111 -43
  107. package/packages/lsp-tools-mcp/dist/cli.js +77 -23
  108. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1 -1
  109. package/packages/lsp-tools-mcp/dist/mcp.js +77 -23
  110. package/packages/lsp-tools-mcp/dist/tools.js +77 -23
  111. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  112. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +268 -92
  113. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  114. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  115. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +7 -7
  116. package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +3 -0
  117. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +220 -10
  118. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +220 -10
  119. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  121. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  122. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  123. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  124. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  125. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  126. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +3 -17
  127. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  128. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +135 -67
  129. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  132. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/rules/test/bundled-rules-priority.test.ts +11 -16
  134. package/packages/omo-codex/plugin/components/rules/test/bundled-rules.test.ts +16 -23
  135. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +9 -7
  136. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-context.test.ts +0 -6
  137. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +6 -4
  138. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-directive.test.ts +12 -9
  139. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +28 -37
  140. package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +37 -69
  141. package/packages/omo-codex/plugin/components/rules/test/hook-output.test.ts +2 -3
  142. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +1 -15
  143. package/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md +4 -2
  144. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +5 -1
  145. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +2 -1
  146. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +18 -0
  147. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  148. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  149. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +21 -0
  150. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +0 -3
  151. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +107 -16
  152. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  153. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  154. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +3 -9
  155. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +24 -12
  156. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +24 -12
  157. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  158. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  159. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +2 -0
  160. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +1 -1
  161. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
  162. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +1 -1
  163. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +1 -1
  164. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-low.toml +1 -1
  165. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +1 -1
  166. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  167. package/packages/omo-codex/plugin/components/ultrawork/directive.md +9 -2
  168. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  169. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  170. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +9 -2
  171. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +3 -3
  172. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +30 -6
  173. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -1
  174. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +3 -3
  175. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +0 -136
  177. package/packages/omo-codex/plugin/components/ultrawork/test/skill-pointer.test.ts +0 -2
  178. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
  179. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
  180. package/packages/omo-codex/plugin/components/ulw-loop/README.md +11 -11
  181. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +9 -2
  182. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  184. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +9 -9
  185. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +1 -1
  186. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +66 -66
  187. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +4 -4
  188. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-hook.js +1 -1
  189. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -1
  190. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +1 -1
  191. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +1 -1
  192. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +2 -2
  193. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  194. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  195. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
  196. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/define-goal.md +108 -0
  197. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +23 -25
  198. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +1 -1
  199. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +9 -9
  200. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +1 -1
  201. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +1 -1
  202. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +4 -4
  203. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts +1 -1
  204. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
  205. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +1 -1
  206. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +1 -1
  207. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +2 -2
  208. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-continuation.test.ts +0 -1
  209. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +2 -2
  210. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +1 -1
  211. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-helpers.test.ts +2 -2
  212. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-kind-guidance.test.ts +1 -1
  213. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +2 -2
  214. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-hook.test.ts +2 -5
  215. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +1 -1
  216. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +7 -40
  217. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +1 -1
  218. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +1 -1
  219. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +1 -1
  220. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +1 -1
  221. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +4 -5
  222. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  223. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  224. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  225. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  226. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  227. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  228. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  229. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  230. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  231. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  232. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  233. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  234. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  235. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  236. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  237. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  238. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  239. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  240. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  241. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  242. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  243. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  244. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  245. package/packages/omo-codex/plugin/package-lock.json +20 -20
  246. package/packages/omo-codex/plugin/package.json +1 -1
  247. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +4 -4
  248. package/packages/omo-codex/plugin/skills/ast-grep/SOURCE +1 -1
  249. package/packages/omo-codex/plugin/skills/ast-grep/install.ps1 +2 -2
  250. package/packages/omo-codex/plugin/skills/ast-grep/install.sh +1 -1
  251. package/packages/omo-codex/plugin/skills/ast-grep/references/install.md +2 -2
  252. package/packages/omo-codex/plugin/skills/ast-grep/tests/smoke.sh +1 -1
  253. package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +3 -2
  254. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  255. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/senpi.md +4 -4
  256. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
  257. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +10 -7
  258. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +1 -0
  259. package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +80 -0
  260. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +54 -9
  261. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
  262. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +2 -2
  263. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +179 -0
  264. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/__main__.py +8 -1
  265. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/bias_check.py +11 -0
  266. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/fetch_chain.py +90 -52
  267. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/result_schema.py +10 -1
  268. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/surrogate.py +214 -0
  269. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/surrogates.yaml +60 -0
  270. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +1 -1
  271. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/amp_redirect_stub.html +7 -0
  272. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/search_interstitial.html +19 -0
  273. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_available.json +1 -0
  274. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/fixtures/wayback_snapshot.html +1128 -0
  275. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate.py +252 -0
  276. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_surrogate_validators.py +78 -0
  277. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/validators.py +46 -0
  278. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/waf_detector.py +1 -1
  279. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/waf_profiles.yaml +10 -5
  280. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/agent-reach/social.md +1 -1
  281. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +13 -11
  282. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  283. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/cache-archive.md +51 -50
  284. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/fallback.md +1 -1
  285. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/jina.md +8 -2
  286. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/naver.md +1 -1
  287. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/twitter.md +3 -3
  288. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +9 -2
  289. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
  290. package/packages/omo-codex/plugin/skills/ulw-loop/references/define-goal.md +108 -0
  291. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +23 -25
  292. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +3 -3
  293. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +30 -6
  294. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -1
  295. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +3 -3
  296. package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  297. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +15 -10
  298. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +19 -173
  299. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +4 -24
  300. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +175 -13
  301. package/packages/omo-codex/plugin/test/aggregate.test.mjs +78 -2
  302. package/packages/omo-codex/plugin/test/auto-update-release-notes.test.mjs +19 -33
  303. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +12 -12
  304. package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +36 -4
  305. package/packages/omo-codex/plugin/test/lcx-contribute-bug-fix-template.test.mjs +21 -27
  306. package/packages/omo-codex/plugin/test/scaffold-plan.test.mjs +0 -36
  307. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +101 -0
  308. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +4 -4
  309. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +1 -119
  310. package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +0 -40
  311. package/packages/omo-codex/plugin/test/teammode-communication.test.mjs +6 -62
  312. package/packages/omo-codex/plugin/test/teammode-thread-links.test.mjs +3 -36
  313. package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +0 -44
  314. package/packages/omo-codex/plugin/test/teammode-worktree.test.mjs +2 -6
  315. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +0 -3
  316. package/packages/omo-codex/plugin/test/ulw-plan-review-state-contract.test.mjs +0 -3
  317. package/packages/omo-codex/scripts/install-bin-links.test.mjs +56 -2
  318. package/packages/omo-codex/scripts/install-delegated-command.test.mjs +6 -6
  319. package/packages/omo-codex/scripts/install-dist/install-local.mjs +170 -78
  320. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +7 -2
  321. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +4 -4
  322. package/packages/omo-codex/scripts/install-local.test.mjs +5 -2
  323. package/packages/shared-skills/index.mjs +19 -1
  324. package/packages/shared-skills/skills/ast-grep/SOURCE +1 -1
  325. package/packages/shared-skills/skills/ast-grep/install.ps1 +2 -2
  326. package/packages/shared-skills/skills/ast-grep/install.sh +1 -1
  327. package/packages/shared-skills/skills/ast-grep/references/install.md +2 -2
  328. package/packages/shared-skills/skills/ast-grep/tests/smoke.sh +1 -1
  329. package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +3 -2
  330. package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  331. package/packages/shared-skills/skills/coding-agent-sessions/references/senpi.md +4 -4
  332. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
  333. package/packages/shared-skills/skills/frontend/SKILL.md +10 -7
  334. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +1 -0
  335. package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +80 -0
  336. package/packages/shared-skills/skills/start-work/SKILL.md +54 -9
  337. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
  338. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +2 -2
  339. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +179 -0
  340. package/packages/shared-skills/skills/ultimate-browsing/engine/__main__.py +8 -1
  341. package/packages/shared-skills/skills/ultimate-browsing/engine/bias_check.py +11 -0
  342. package/packages/shared-skills/skills/ultimate-browsing/engine/fetch_chain.py +90 -52
  343. package/packages/shared-skills/skills/ultimate-browsing/engine/result_schema.py +10 -1
  344. package/packages/shared-skills/skills/ultimate-browsing/engine/surrogate.py +214 -0
  345. package/packages/shared-skills/skills/ultimate-browsing/engine/surrogates.yaml +60 -0
  346. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +1 -1
  347. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/fixtures/amp_redirect_stub.html +7 -0
  348. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/fixtures/search_interstitial.html +19 -0
  349. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/fixtures/wayback_available.json +1 -0
  350. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/fixtures/wayback_snapshot.html +1128 -0
  351. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_surrogate.py +252 -0
  352. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_surrogate_validators.py +78 -0
  353. package/packages/shared-skills/skills/ultimate-browsing/engine/validators.py +46 -0
  354. package/packages/shared-skills/skills/ultimate-browsing/engine/waf_detector.py +1 -1
  355. package/packages/shared-skills/skills/ultimate-browsing/engine/waf_profiles.yaml +10 -5
  356. package/packages/shared-skills/skills/ultimate-browsing/references/agent-reach/social.md +1 -1
  357. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +13 -11
  358. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +4 -4
  359. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/cache-archive.md +51 -50
  360. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/fallback.md +1 -1
  361. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/jina.md +8 -2
  362. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/naver.md +1 -1
  363. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/twitter.md +3 -3
  364. package/packages/shared-skills/skills/ulw-plan/SKILL.md +3 -3
  365. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +30 -6
  366. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -1
  367. package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +3 -3
  368. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  369. package/packages/shared-skills/skills/ulw-research/SKILL.md +15 -10
  370. package/postinstall.mjs +6 -0
  371. package/dist/tools/call-omo-agent/background-agent-executor.d.ts +0 -5
  372. package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +0 -92
  373. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +0 -314
  374. package/packages/omo-codex/plugin/test/ulw-plan-scope-contract.test.mjs +0 -24
@@ -0,0 +1,179 @@
1
+ # ultimate-browsing/engine — Generic WAF-Profile Fetch Chain (Python)
2
+
3
+ **Generated:** 2026-08-10 / 38d268995
4
+
5
+ ## UPSTREAM BASELINE AND VERSION POLICY
6
+
7
+ **READ THIS BEFORE TOUCHING `engine/**` OR PROPOSING AN UPSTREAM SYNC.**
8
+
9
+ This engine is NOT project-original code. It is a vendored-and-modified snapshot of
10
+ [fivetaku/insane-search](https://github.com/fivetaku/insane-search), and the version
11
+ we run on is a deliberate choice, not an accident of neglect.
12
+
13
+ ### The pin
14
+
15
+ | Fact | Value |
16
+ |---|---|
17
+ | Upstream project | `https://github.com/fivetaku/insane-search` |
18
+ | Vendoring commit | `a4e4ed797` (2026-06-21) `feat(ultimate-browsing): vendor insane-search engine (junk-excluded)` |
19
+ | De-personalization | `4743199a5` (2026-06-21) |
20
+ | Pinned upstream baseline | upstream state as of 2026-06-21, **pre-0.7.0** (0.7.0 is dated 2026-06-22) |
21
+ | Re-vendors since | none — every later change here is ours |
22
+
23
+ We intentionally track a **pinned baseline plus local divergence**, not upstream HEAD.
24
+ There is no submodule and no automated drift check for this engine (unlike the
25
+ `frontend` skill's upstream submodules): the vendored files ARE the source of truth,
26
+ and upstream is a reference we port FROM, deliberately, file by file.
27
+
28
+ ### Why we do not blind-rebase onto upstream HEAD
29
+
30
+ 1. **Upstream reset its public history.** On 2026-08-06 upstream published a single
31
+ squashed commit, `019ee16 refactor: reset public history at 0.14.0`, discarding the
32
+ prior public history through 0.13.x. There is no upstream commit graph to rebase onto and no
33
+ way to cherry-pick an individual upstream change by sha — only whole-file diffing
34
+ against a moving HEAD.
35
+ 2. **Upstream 0.14.0 REMOVED capability.** The endpoint-mining / internal-API
36
+ auto-derivation / site-recipe subsystems that upstream carried publicly between
37
+ 0.12.0 and 0.14.0 are gone from upstream HEAD. Syncing to HEAD is therefore not
38
+ strictly an upgrade: parts of it are a downgrade relative to the intermediate
39
+ versions, and none of it is recoverable from the reset history.
40
+ 3. **Our tree diverged on purpose.** The KEEP list below is functionality upstream
41
+ never had. A wholesale overwrite with upstream HEAD would silently delete it.
42
+ 4. **Different threat model.** Our engine ships inside a published npm package and a
43
+ public marketplace mirror, under a CI no-site-name gate and a de-personalization
44
+ deny-list. Upstream carries neither constraint, so upstream code is not
45
+ drop-in-shippable here.
46
+
47
+ ### KEEP — our divergences a re-vendor MUST NOT regress
48
+
49
+ These exist only in our tree. Any upstream sync that removes or bypasses one of them
50
+ is a regression, not an upgrade:
51
+
52
+ - **Phase 2.5 surrogate retrieval** (`surrogate.py`, `surrogates.yaml`) — archive /
53
+ reader / proxy routes tried before paying for a browser spin-up, with per-entry
54
+ `last_verified` staleness handling and `--allow-proxy` gating.
55
+ - **Provenance / trust contract** (`result_schema.py`) — `Provenance` and `Trust`
56
+ literals on every result, so a snapshot can never be reported as the live page.
57
+ - **Surrogate dead-end validation** (L1.5 in `validators.py`) — interstitial titles and
58
+ AMP-style redirect stubs rejected instead of returned as content.
59
+ - **The no-site-name rule and its CI gate** (`bias_check.py`) — zero hard-coded site
60
+ names, brands, or target domains in `engine/**`.
61
+ - **Module split of the fetch chain** — `curl_probe` / `referers` / `url_transforms` /
62
+ `waf_detector` / `validators` / `executor` / `summary` as separate modules rather than
63
+ one monolith.
64
+ - **The Python test suite** under `engine/tests/` with its HTML/JSON fixtures.
65
+ - **De-personalization** — no personal absolute paths, no personal auth token literals,
66
+ no personal browser choice; enforced by `depersonalization-gate.test.ts`.
67
+ - **Skill-level layering** — the engine is Tier 1 under a router that also owns Tier 1.5
68
+ (agent-reach) and Tier 2 (CloakBrowser + agent-browser). Upstream has no such tiering.
69
+
70
+ ### WANT — upstream improvements worth porting forward
71
+
72
+ Our snapshot predates these; they are wanted, and each must be ported as a reviewed,
73
+ site-agnostic change that preserves every KEEP item above. Port individually; never as
74
+ a tree overwrite:
75
+
76
+ - **Content quality**: dedicated markdown conversion of fetched HTML, main-content
77
+ extraction, PDF text extraction, and JSON-LD rescue when the HTML body is thin.
78
+ - **Transient-failure retry** and **render-merge** of statically fetched HTML with the
79
+ browser-rendered DOM.
80
+ - **Differential block classification** — distinguishing a bot-detection block from an
81
+ infrastructure or authentication failure, instead of collapsing both into `challenge`.
82
+ - **Additional stealth fetch backends** beyond the current Playwright templates, and
83
+ additional WAF vendor profiles.
84
+ - **Per-host route learning** — remembering which route succeeded for a host, with a TTL
85
+ and a bounded store. Must stay runtime state, never committed site knowledge (R4).
86
+ - **Engine-level Phase 0 routing** — the official-public-API preference is currently only
87
+ a documented rule (R5) the agent can skip; upstream moved it into code so it cannot be
88
+ skipped. Worth adopting.
89
+
90
+ ### OUT OF SCOPE
91
+
92
+ - **The removed upstream endpoint-mining / internal-API auto-derivation / site-recipe
93
+ subsystems.** They are absent from upstream HEAD and are not reconstructed here. They
94
+ also sit against R3/R4 and R7's anti-bias rule: discovered internal endpoints are
95
+ runtime findings, never committed engine knowledge.
96
+ - **Any upstream code carrying site-specific selectors, domains, or brand names.** It
97
+ fails `bias_check.py` at the door; re-derive it site-agnostically or leave it out.
98
+ - **Automated upstream tracking.** No submodule, no drift check, no auto-bump. Syncing is
99
+ a deliberate, reviewed, human-initiated act.
100
+
101
+ ### THE SYNC RULE
102
+
103
+ Any future upstream sync preserves BOTH sides. Concretely:
104
+
105
+ 1. Diff the specific upstream capability you want against our tree — do not overwrite
106
+ files wholesale, and never `git checkout` upstream over `engine/`.
107
+ 2. Port it as its own reviewed change, keeping every KEEP item intact.
108
+ 3. Re-run `python3 engine/bias_check.py` and the `engine/tests/` suite; a port that
109
+ introduces a site name or breaks a fixture does not ship.
110
+ 4. Update the pin table above (baseline, date, what was ported) in the same change, plus
111
+ the provenance section of [`../ATTRIBUTION.md`](../ATTRIBUTION.md).
112
+ 5. If a port must drop a KEEP item, say so explicitly in the PR and get it agreed first —
113
+ silent regressions of the KEEP list are the failure mode this policy exists to prevent.
114
+
115
+ ## OVERVIEW
116
+
117
+ A 17-module Python package embedded in the `ultimate-browsing` skill: a site-agnostic fetch chain that escalates from a cheap curl probe to a real browser, with declarative WAF and surrogate registries. Not "optional scripts" — it has its own CLI entry (`python3 -m engine URL`), two YAML config schemas, a 4-file test suite, and a standalone CI guard. Package exports (`__init__.py`): `fetch`, `FetchResult`, `Attempt`, `Verdict`, `ValidationResult`, `validate`, `CHALLENGE_MARKERS`, `detect`, `TRANSFORMS`, `apply_transform`.
118
+
119
+ ## THE NO-SITE-NAME RULE (enforced in CI)
120
+
121
+ `engine/**` must contain **zero** hard-coded site names, brands, or target domains. Site specifics belong to runtime hints or observations, never to code. `bias_check.py` is a standalone scanner enforcing this: a brand denylist, a URL regex scan, an allowlist for genuine infrastructure hosts (archive.org, r.jina.ai, google.com, httpbin.org, relay.invalid), and a `# NOTE-BIAS-OK` comment convention for legitimate exemptions such as test fixtures.
122
+
123
+ ```bash
124
+ python3 engine/bias_check.py # fails on any site-specific leak
125
+ ```
126
+
127
+ ## FETCH CHAIN PHASES
128
+
129
+ ```
130
+ fetch(url, ...) # fetch_chain.py
131
+ Phase 1 curl_probe.py — curl_cffi TLS-impersonation probe
132
+ Phase 2 grid — referer/transform/device attempt grid
133
+ Phase 2.5 surrogate.py — third-party archive/reader/proxy routes
134
+ Phase 3 executor.py — capability-matched Playwright fallback
135
+ ```
136
+
137
+ Ordering is **not** hardcoded: each `waf_profiles.yaml` profile carries a `fallback_when_challenge` list that drives the ladder. `surrogate_wayback` precedes browser executors in every profile, so archives are tried before paying for a browser spin-up.
138
+
139
+ ## PROVENANCE / TRUST CONTRACT
140
+
141
+ `result_schema.py` puts two literals on every `FetchResult`:
142
+
143
+ - `Provenance = "live" | "snapshot" | "proxy"`
144
+ - `Trust = "origin" | "archive" | "untrusted"`
145
+
146
+ A `snapshot` result carries `snapshot_timestamp` and **must** be cited with that timestamp — never presented as the live page. `surrogates.yaml` `kind` fixes these values: `archive` -> snapshot/archive, `reader` -> live, `proxy` -> proxy/untrusted.
147
+
148
+ ## SURROGATE REGISTRY (`surrogates.yaml`)
149
+
150
+ Site-agnostic infrastructure only. Every entry carries `last_verified` (ISO date); entries older than 90 days are deprioritized and flagged, because surrogate routes rot (a 2026-08 probe found 4 of 6 known routes dead or stubbed). `proxy` routes are MITM by construction: they require the explicit `--allow-proxy` flag and never receive `Cookie` or `Authorization` headers. Every surrogate response is re-validated with `target_url` set, so an interstitial or a redirect stub is rejected instead of returned as content.
151
+
152
+ ## VALIDATOR LAYERS (`validators.py`)
153
+
154
+ ```
155
+ L1 challenge markers (CHALLENGE_MARKERS)
156
+ L1.5 surrogate dead ends — interstitial titles + AMP-style redirect stubs
157
+ (is_redirect_stub(), needs target_url)
158
+ L2 size/shape fingerprints
159
+ L3+ content checks
160
+ ```
161
+
162
+ ## CLI
163
+
164
+ ```bash
165
+ python3 -m engine URL [--selector S] [--device auto|desktop|mobile]
166
+ [--timeout 25] [--max-attempts 12]
167
+ [--no-playwright] [--allow-proxy] [--json] [--trace]
168
+ ```
169
+
170
+ ## TESTS
171
+
172
+ `tests/` — `test_surrogate.py` (staleness, proxy gating, short-circuit), `test_surrogate_validators.py`, `test_fetch_chain.py`, `test_playwright_templates.py`, plus HTML/JSON fixtures under `tests/fixtures/`.
173
+
174
+ ## NOTES
175
+
176
+ - `summary.py` emits an **R7 API-first hint** after >=3 challenge verdicts against a known WAF profile: look for `/api/`, `/graphql`, or `.json` endpoints, which usually carry weaker WAF protection than the HTML surface.
177
+ - `templates/` holds the Playwright JS templates (`playwright_real_chrome.js`, `playwright_mobile_chrome.js`) the executor drives.
178
+ - `url_transforms.py` transforms stay domain-agnostic (`mobile_subdomain`, `am_prefix`, `drop_www`).
179
+ - Parent: [`packages/shared-skills/AGENTS.md`](../../../AGENTS.md).
@@ -39,6 +39,10 @@ def build_parser() -> argparse.ArgumentParser:
39
39
  help="Upper bound across all phases (default 12).")
40
40
  p.add_argument("--no-playwright", action="store_true",
41
41
  help="Skip Playwright fallback (curl-only).")
42
+ p.add_argument("--allow-proxy", action="store_true",
43
+ help=("Enable kind=proxy surrogate entries (raw relay routes; "
44
+ "MITM by construction — never citable alone, never "
45
+ "receives cookies/auth headers). Off by default."))
42
46
  p.add_argument("--json", action="store_true",
43
47
  help="Emit FetchResult as JSON to stdout (content omitted).")
44
48
  p.add_argument("--trace", action="store_true",
@@ -56,6 +60,7 @@ def main(argv: list[str] | None = None) -> int:
56
60
  timeout=args.timeout,
57
61
  max_attempts=args.max_attempts,
58
62
  enable_playwright=not args.no_playwright,
63
+ allow_surrogate_proxy=args.allow_proxy,
59
64
  )
60
65
  except Exception as e:
61
66
  print(f"engine fatal: {type(e).__name__}: {e}", file=sys.stderr)
@@ -96,7 +101,9 @@ def main(argv: list[str] | None = None) -> int:
96
101
  # Default: HTML to stdout, status to stderr.
97
102
  print(result.content, end="")
98
103
  print(f"\n[engine] ok={result.ok} verdict={result.verdict} "
99
- f"profile={result.profile_used} attempts={len(result.trace)}",
104
+ f"profile={result.profile_used} attempts={len(result.trace)} "
105
+ f"provenance={result.provenance}"
106
+ + (f" snapshot_ts={result.snapshot_timestamp}" if result.snapshot_timestamp else ""),
100
107
  file=sys.stderr)
101
108
 
102
109
  return 0 if result.ok else 1
@@ -54,6 +54,14 @@ URL_ALLOWLIST = {
54
54
  "www.google.com", "google.com",
55
55
  # Generic HTTP test endpoint for infrastructure / transport tests.
56
56
  "httpbin.org",
57
+ # Surrogate-tier infrastructure (engine/surrogates.yaml): archives/relays
58
+ # that serve a copy of ANY target URL. Same no-site-preference category as
59
+ # the google.com Referer — they are not target sites.
60
+ "archive.org", "web.archive.org",
61
+ "r.jina.ai",
62
+ # Placeholder host in the proxy-entry shape example; "canonical non-routable
63
+ # doc domain" (.invalid TLD), can never be a real target.
64
+ "relay.invalid",
57
65
  }
58
66
 
59
67
  # Files / dirs that must be clean.
@@ -80,6 +88,9 @@ EXPLICIT_ALLOW_FILES = {
80
88
  # None right now — add if needed with justification.
81
89
  }
82
90
 
91
+ # NOTE-BIAS-OK convention for surrogate tests: fixture URLs (stub/interstitial
92
+ # captures from third-party services) carry the marker on the flagged line.
93
+
83
94
 
84
95
  def _line_is_exempt(line: str, ext: str) -> bool:
85
96
  markers = COMMENT_OK_MARKERS.get(ext, ())
@@ -40,6 +40,7 @@ def fetch(
40
40
  timeout: int = 25,
41
41
  max_attempts: int = 12,
42
42
  enable_playwright: bool = True, # hook left for executor module
43
+ allow_surrogate_proxy: bool = False, # opt-in: kind=proxy registry entries
43
44
  ) -> FetchResult:
44
45
  """Fetch `url` using the generic grid.
45
46
 
@@ -175,73 +176,110 @@ def fetch(
175
176
  if att.verdict in (Verdict.STRONG_OK.value, Verdict.WEAK_OK.value):
176
177
  return _build_result(resp, att, trace, profile_used=profile_id)
177
178
 
178
- # -------- Phase 3: Playwright fallback (profile-driven order) -----------
179
- if enable_playwright:
180
- try:
181
- from .executor import run_playwright_fallback # lazy import
182
- # Honour profile's `fallback_when_challenge` list — iterate the
183
- # caller-declared order instead of capability-inferred single pick.
184
- fb_profile = load_profile(profile_used or "unknown_challenge", profiles=profiles)
185
- fb_order = fb_profile.get("fallback_when_challenge") or ["playwright_real_chrome"]
186
- pw_attempt = None
187
- pw_content = ""
188
- for fb_name in fb_order:
189
- if fb_name == "curl_grid_exhaust":
190
- # Already performed in Phase 2; nothing more to do here.
191
- continue
192
- pw_attempt, pw_content = run_playwright_fallback(
179
+ # -------- Phase 2.5 + 3: fallback ladder (profile-driven order) ---------
180
+ # Surrogate routes are NOT browser work, so they run even when the caller
181
+ # disabled Playwright; only browser executors honour `enable_playwright`.
182
+ try:
183
+ # Honour profile's `fallback_when_challenge` list — iterate the
184
+ # caller-declared order instead of capability-inferred single pick.
185
+ fb_profile = load_profile(profile_used or "unknown_challenge", profiles=profiles)
186
+ fb_order = fb_profile.get("fallback_when_challenge") or ["playwright_real_chrome"]
187
+ pw_attempt = None
188
+ pw_content = ""
189
+ for fb_name in fb_order:
190
+ if fb_name == "curl_grid_exhaust":
191
+ # Already performed in Phase 2; nothing more to do here.
192
+ continue
193
+ if fb_name.startswith("surrogate_"):
194
+ # Phase 2.5: archive/reader/proxy routes serving a copy of
195
+ # the target. Cheaper than a browser; provenance-labeled.
196
+ from . import surrogate as surrogate_mod
197
+ s_atts, s_meta, s_content = surrogate_mod.run_surrogate(
193
198
  url,
194
- profile_id=profile_used or "unknown_challenge",
199
+ registry=surrogate_mod.load_surrogates(),
200
+ allow_proxy=allow_surrogate_proxy,
201
+ timeout=timeout,
195
202
  success_selectors=success_selectors,
196
- device_class=device_class,
197
- force_executor=fb_name,
198
203
  )
199
- trace.append(pw_attempt)
200
- if pw_attempt.verdict in (Verdict.STRONG_OK.value, Verdict.WEAK_OK.value):
204
+ trace.extend(s_atts)
205
+ s_att = s_atts[-1]
206
+ if s_att.verdict in (Verdict.STRONG_OK.value, Verdict.WEAK_OK.value):
201
207
  return FetchResult(
202
208
  ok=True,
203
- content=pw_content,
204
- final_url=pw_attempt.url,
205
- verdict=pw_attempt.verdict,
209
+ content=s_content,
210
+ final_url=s_att.url,
211
+ verdict=s_att.verdict,
206
212
  profile_used=profile_used,
207
213
  trace=trace,
208
- summary=f"Playwright fallback succeeded via {fb_name}",
214
+ summary=(
215
+ f"surrogate {s_meta.get('surrogate')} succeeded"
216
+ f" (provenance={s_meta.get('provenance')}"
217
+ + (f" snapshot_ts={s_meta.get('snapshot_timestamp')})" if s_meta.get('snapshot_timestamp') else ")")
218
+ ),
219
+ provenance=str(s_meta.get("provenance", "live")),
220
+ snapshot_timestamp=s_meta.get("snapshot_timestamp"),
221
+ trust=str(s_meta.get("trust", "origin")),
209
222
  )
210
- # Synthesize a placeholder if no iteration ran (empty list).
211
- if pw_attempt is None:
212
- pw_attempt = Attempt(
213
- phase="fallback",
214
- executor="none",
215
- url=url,
216
- url_transform="original",
217
- impersonate=None,
218
- referer="",
219
- verdict=Verdict.UNKNOWN.value,
220
- error="profile has empty fallback_when_challenge",
223
+ continue
224
+ if not enable_playwright:
225
+ continue
226
+ from .executor import run_playwright_fallback # lazy import
227
+ pw_attempt, pw_content = run_playwright_fallback(
228
+ url,
229
+ profile_id=profile_used or "unknown_challenge",
230
+ success_selectors=success_selectors,
231
+ device_class=device_class,
232
+ force_executor=fb_name,
233
+ )
234
+ trace.append(pw_attempt)
235
+ if pw_attempt.verdict in (Verdict.STRONG_OK.value, Verdict.WEAK_OK.value):
236
+ return FetchResult(
237
+ ok=True,
238
+ content=pw_content,
239
+ final_url=pw_attempt.url,
240
+ verdict=pw_attempt.verdict,
241
+ profile_used=profile_used,
242
+ trace=trace,
243
+ summary=f"Playwright fallback succeeded via {fb_name}",
221
244
  )
222
- trace.append(pw_attempt)
223
- except ImportError:
224
- trace.append(Attempt(
245
+ # Synthesize a placeholder only when the profile genuinely offers no
246
+ # fallback route. Entries skipped because the caller disabled the
247
+ # browser are a caller choice, not a profile defect — no trace noise.
248
+ actionable = [n for n in fb_order if n != "curl_grid_exhaust"]
249
+ if pw_attempt is None and not actionable:
250
+ pw_attempt = Attempt(
225
251
  phase="fallback",
226
- executor="playwright",
252
+ executor="none",
227
253
  url=url,
228
254
  url_transform="original",
229
255
  impersonate=None,
230
256
  referer="",
231
257
  verdict=Verdict.UNKNOWN.value,
232
- error="executor module not available",
233
- ))
234
- except (RuntimeError, OSError) as e:
235
- trace.append(Attempt(
236
- phase="fallback",
237
- executor="playwright",
238
- url=url,
239
- url_transform="original",
240
- impersonate=None,
241
- referer="",
242
- verdict=Verdict.UNKNOWN.value,
243
- error=f"{type(e).__name__}:{str(e)[:200]}",
244
- ))
258
+ error="profile has empty fallback_when_challenge",
259
+ )
260
+ trace.append(pw_attempt)
261
+ except ImportError:
262
+ trace.append(Attempt(
263
+ phase="fallback",
264
+ executor="playwright",
265
+ url=url,
266
+ url_transform="original",
267
+ impersonate=None,
268
+ referer="",
269
+ verdict=Verdict.UNKNOWN.value,
270
+ error="executor module not available",
271
+ ))
272
+ except (RuntimeError, OSError) as e:
273
+ trace.append(Attempt(
274
+ phase="fallback",
275
+ executor="playwright",
276
+ url=url,
277
+ url_transform="original",
278
+ impersonate=None,
279
+ referer="",
280
+ verdict=Verdict.UNKNOWN.value,
281
+ error=f"{type(e).__name__}:{str(e)[:200]}",
282
+ ))
245
283
 
246
284
  # -------- Give up, return best we have ----------------------------------
247
285
  summary = format_summary(trace, profile_used)
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import asdict, dataclass, field
4
- from typing import Optional
4
+ from typing import Literal, Optional
5
+
6
+ Provenance = Literal["live", "snapshot", "proxy"]
7
+ Trust = Literal["origin", "archive", "untrusted"]
5
8
 
6
9
 
7
10
  @dataclass
@@ -32,6 +35,9 @@ class FetchResult:
32
35
  profile_used: Optional[str] = None
33
36
  trace: list[Attempt] = field(default_factory=list)
34
37
  summary: str = ""
38
+ provenance: str = "live" # live | snapshot | proxy
39
+ snapshot_timestamp: Optional[str] = None # archive's own timestamp, when snapshot
40
+ trust: str = "origin" # origin | archive | untrusted
35
41
 
36
42
  def to_dict(self) -> dict:
37
43
  return {
@@ -41,5 +47,8 @@ class FetchResult:
41
47
  "profile_used": self.profile_used,
42
48
  "trace": [a.to_dict() for a in self.trace],
43
49
  "summary": self.summary,
50
+ "provenance": self.provenance,
51
+ "snapshot_timestamp": self.snapshot_timestamp,
52
+ "trust": self.trust,
44
53
  "content_length": len(self.content),
45
54
  }
@@ -0,0 +1,214 @@
1
+ """Phase 2.5 surrogate retrieval: archive / reader / proxy routes for blocked origins.
2
+
3
+ When the curl grid cannot fetch the live page, this module reads a declarative
4
+ registry (engine/surrogates.yaml) and tries third-party routes that serve a
5
+ copy of the target instead. Provenance semantics are contractual:
6
+
7
+ kind=archive -> provenance="snapshot" (+ archive's own timestamp), trust=archive
8
+ kind=reader -> provenance="live" (server-side re-render of the live page)
9
+ kind=proxy -> provenance="proxy", trust=untrusted; requires allow_proxy=True
10
+ and never sends Cookie/Authorization headers (relay = MITM)
11
+
12
+ Every response body passes through validators.validate() with target_url set,
13
+ so AMP-style redirect stubs and search-engine interstitials are rejected
14
+ rather than recorded as wins. Entries whose `last_verified` is older than
15
+ MAX_STALE_DAYS are tried after fresh ones (routes rot faster than they are
16
+ maintained).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import time
23
+ from datetime import date
24
+
25
+ try:
26
+ import yaml
27
+ except ImportError:
28
+ yaml = None
29
+
30
+ from .result_schema import Attempt
31
+ from .validators import Verdict, validate
32
+
33
+ SURROGATES_PATH = os.path.join(os.path.dirname(__file__), "surrogates.yaml")
34
+ MAX_STALE_DAYS = 90
35
+
36
+ DEFAULT_SURROGATES: dict = {
37
+ "wayback": {
38
+ "kind": "archive",
39
+ "trust": "archive",
40
+ "enabled": True,
41
+ "last_verified": "2026-08-09",
42
+ "discovery": {
43
+ "url": "https://archive.org/wayback/available?url={target}",
44
+ "flag_pointer": "archived_snapshots.closest.available",
45
+ "snapshot_url_pointer": "archived_snapshots.closest.url",
46
+ "snapshot_timestamp_pointer": "archived_snapshots.closest.timestamp",
47
+ },
48
+ "min_body_bytes": 3072,
49
+ },
50
+ }
51
+
52
+
53
+ def load_surrogates(path: str = SURROGATES_PATH) -> dict:
54
+ if yaml is None:
55
+ return dict(DEFAULT_SURROGATES)
56
+ try:
57
+ with open(path, encoding="utf-8") as fh:
58
+ loaded = yaml.safe_load(fh) or {}
59
+ except (OSError, yaml.YAMLError):
60
+ return dict(DEFAULT_SURROGATES)
61
+ if not isinstance(loaded, dict):
62
+ return dict(DEFAULT_SURROGATES)
63
+ usable = {k: v for k, v in loaded.items() if isinstance(v, dict) and k}
64
+ return usable or dict(DEFAULT_SURROGATES)
65
+
66
+
67
+ def is_stale(entry: dict, max_age_days: int = MAX_STALE_DAYS, today: date | None = None) -> bool:
68
+ verified = entry.get("last_verified")
69
+ if isinstance(verified, date):
70
+ parsed = verified
71
+ elif isinstance(verified, str):
72
+ try:
73
+ parsed = date.fromisoformat(verified)
74
+ except ValueError:
75
+ return True
76
+ else:
77
+ return True
78
+ return ((today or date.today()) - parsed).days > max_age_days
79
+
80
+
81
+ def _http_get(url: str, *, headers: dict | None = None, timeout: int = 25):
82
+ try:
83
+ from curl_cffi import requests as cffi_requests
84
+ except ImportError as exc:
85
+ raise RuntimeError("curl_cffi not installed") from exc
86
+ return cffi_requests.get(
87
+ url,
88
+ impersonate="chrome",
89
+ headers=headers or {"Accept": "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8"},
90
+ timeout=timeout,
91
+ allow_redirects=True,
92
+ )
93
+
94
+
95
+ def _json_pointer(obj, dotted: str):
96
+ current = obj
97
+ for part in dotted.split("."):
98
+ if not isinstance(current, dict):
99
+ return None
100
+ current = current.get(part)
101
+ return current
102
+
103
+
104
+ def _provenance_of(kind: str) -> str:
105
+ if kind == "archive":
106
+ return "snapshot"
107
+ if kind == "proxy":
108
+ return "proxy"
109
+ return "live"
110
+
111
+
112
+ def _skip_reason(name: str, entry: dict, allow_proxy: bool) -> str | None:
113
+ kind = entry.get("kind", "archive")
114
+ if kind == "proxy" and not allow_proxy:
115
+ return "proxy_requires_allow_proxy_flag"
116
+ required_env = entry.get("enabled_env")
117
+ if required_env and not os.environ.get(str(required_env)):
118
+ return f"missing_env:{required_env}"
119
+ if entry.get("enabled") is False and kind != "proxy":
120
+ return "disabled"
121
+ return None
122
+
123
+
124
+ def _attempt(name: str, url: str, verdict: Verdict, error=None, status: int = 0, size: int = 0, elapsed: float = 0.0) -> Attempt:
125
+ return Attempt(
126
+ phase="surrogate",
127
+ executor=f"surrogate_{name}",
128
+ url=url,
129
+ url_transform="original",
130
+ impersonate=None,
131
+ referer="",
132
+ status=status,
133
+ body_size=size,
134
+ verdict=verdict.value,
135
+ error=(str(error)[:200] if error else None),
136
+ elapsed_s=round(elapsed, 3),
137
+ )
138
+
139
+
140
+ def _resolve_target(name: str, entry: dict, target: str, timeout: int) -> tuple[str | None, str | None]:
141
+ discovery = entry.get("discovery")
142
+ if not discovery:
143
+ host_rotation = entry.get("host_rotation") or [None]
144
+ host = host_rotation[0]
145
+ template = entry.get("fetch")
146
+ if not template or host is None and "{host}" in template:
147
+ return None, None
148
+ return template.format(host=host, target=target), None
149
+ discovery_url = discovery.get("url", "").format(target=target)
150
+ resp = _http_get(discovery_url, timeout=timeout)
151
+ payload = resp.json()
152
+ if discovery.get("flag_pointer") and not _json_pointer(payload, discovery["flag_pointer"]):
153
+ return None, None
154
+ snap_url = _json_pointer(payload, discovery.get("snapshot_url_pointer", ""))
155
+ timestamp = _json_pointer(payload, discovery.get("snapshot_timestamp_pointer", "")) or None
156
+ return snap_url, timestamp
157
+
158
+
159
+ def run_surrogate(
160
+ target: str,
161
+ *,
162
+ registry: dict | None = None,
163
+ allow_proxy: bool = False,
164
+ timeout: int = 25,
165
+ success_selectors: list[str] | None = None,
166
+ ) -> tuple[list[Attempt], dict, str]:
167
+ registry = dict(registry) if registry else load_surrogates()
168
+ ordered = sorted(registry.items(), key=lambda kv: int(is_stale(kv[1])))
169
+ live_meta = {"provenance": "live", "snapshot_timestamp": None, "trust": "origin", "surrogate": None}
170
+ attempts: list[Attempt] = []
171
+
172
+ for name, entry in ordered:
173
+ kind = entry.get("kind", "archive")
174
+ reason = _skip_reason(name, entry, allow_proxy)
175
+ if reason:
176
+ attempts.append(_attempt(name, target, Verdict.UNKNOWN, error=f"skipped:{reason}"))
177
+ continue
178
+ started = time.time()
179
+ try:
180
+ fetch_url, timestamp = _resolve_target(name, entry, target, timeout)
181
+ except Exception as exc:
182
+ attempts.append(_attempt(name, target, Verdict.UNKNOWN, error=f"{type(exc).__name__}:{exc}", elapsed=time.time() - started))
183
+ continue
184
+ if not fetch_url:
185
+ attempts.append(_attempt(name, target, Verdict.UNKNOWN, error="no snapshot available", elapsed=time.time() - started))
186
+ continue
187
+ try:
188
+ resp = _http_get(fetch_url, timeout=timeout)
189
+ except Exception as exc:
190
+ attempts.append(_attempt(name, fetch_url, Verdict.UNKNOWN, error=f"{type(exc).__name__}:{exc}", elapsed=time.time() - started))
191
+ continue
192
+ body = getattr(resp, "text", "") or ""
193
+ status = int(getattr(resp, "status_code", 0) or 0)
194
+ min_bytes = int(entry.get("min_body_bytes", 3072))
195
+ att = _attempt(name, fetch_url, Verdict.UNKNOWN, status=status, size=len(body), elapsed=time.time() - started)
196
+ attempts.append(att)
197
+ if status >= 400 or len(body) < min_bytes:
198
+ att.error = f"surrogate_unusable:status={status},size={len(body)}"
199
+ continue
200
+ vr = validate(resp, success_selectors=success_selectors, target_url=target)
201
+ att.verdict = vr.verdict.value
202
+ att.reasons = vr.reasons
203
+ if vr.ok:
204
+ meta = {
205
+ "provenance": _provenance_of(kind),
206
+ "snapshot_timestamp": timestamp if kind == "archive" else None,
207
+ "trust": str(entry.get("trust", "archive")),
208
+ "surrogate": name,
209
+ }
210
+ return attempts, meta, body
211
+
212
+ if not attempts:
213
+ attempts.append(_attempt("none", target, Verdict.UNKNOWN, error="no surrogate entries"))
214
+ return attempts, live_meta, ""