opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8

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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Diagnosis — Causal Loop (C05)
3
+ *
4
+ * Implements a closed diagnostic loop:
5
+ * observation → classify failure class → generate candidate causes →
6
+ * derive discriminating experiment → execute & observe → assess cause.
7
+ *
8
+ * The engine NEVER assumes stealth / anti-bot measures unless the evidence
9
+ * supports it (Chromium golden test: an executable-missing error is not
10
+ * labelled ANTI_BOT just because the agent feels like it).
11
+ */
12
+
13
+ import {
14
+ AssessmentResult,
15
+ } from "../context/assessment-rules.js";
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Failure class taxonomy
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ export const FailureClass = Object.freeze({
22
+ BROWSER_RUNTIME: "BROWSER_RUNTIME",
23
+ MISSING_EXECUTABLE: "MISSING_EXECUTABLE",
24
+ ANTI_BOT: "ANTI_BOT",
25
+ NETWORK_ERROR: "NETWORK_ERROR",
26
+ AUTH: "AUTH",
27
+ CONFIGURATION: "CONFIGURATION",
28
+ RATE_LIMIT: "RATE_LIMIT",
29
+ UNKNOWN: "UNKNOWN",
30
+ });
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Classification rules
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * @typedef {Object} ClassificationRule
38
+ * @property {string} class
39
+ * @property {RegExp[]} errorCodeMatchers
40
+ * @property {RegExp[]} messageMatchers
41
+ * @property {number[]} statusCodes
42
+ */
43
+
44
+ /** @type {ClassificationRule[]} */
45
+ const CLASSIFICATION_RULES = [
46
+ {
47
+ class: FailureClass.MISSING_EXECUTABLE,
48
+ errorCodeMatchers: [
49
+ /executable.*not.*found/i,
50
+ /no.*such.*file/i,
51
+ /enoent/i,
52
+ /browser.*not.*installed/i,
53
+ ],
54
+ messageMatchers: [
55
+ /executable doesn't exist/i,
56
+ /browser.*launch.*failed/i,
57
+ /chromium.*not.*found/i,
58
+ /playwright.*install/i,
59
+ ],
60
+ statusCodes: [],
61
+ },
62
+ {
63
+ class: FailureClass.ANTI_BOT,
64
+ errorCodeMatchers: [/captcha/i, /cloudflare/i, /waf/i, /forbidden/i, /access.denied/i],
65
+ messageMatchers: [
66
+ /captcha/i,
67
+ /cloudflare/i,
68
+ /forbidden/i,
69
+ /403 forbidden/i,
70
+ /access denied/i,
71
+ /bot detected/i,
72
+ /please verify/i,
73
+ ],
74
+ statusCodes: [403, 429],
75
+ },
76
+ {
77
+ class: FailureClass.RATE_LIMIT,
78
+ errorCodeMatchers: [/rate.?limit/i, /too.?many.?requests/i],
79
+ messageMatchers: [/rate.?limit/i, /too.?many.?requests/i, /quota.?exceeded/i],
80
+ statusCodes: [429],
81
+ },
82
+ {
83
+ class: FailureClass.AUTH,
84
+ errorCodeMatchers: [/unauthorized/i, /auth/i, /token/i],
85
+ messageMatchers: [/unauthorized/i, /auth.*fail/i, /invalid.*token/i, /401/i],
86
+ statusCodes: [401, 407],
87
+ },
88
+ {
89
+ class: FailureClass.NETWORK_ERROR,
90
+ errorCodeMatchers: [
91
+ /econnrefused/i,
92
+ /econnreset/i,
93
+ /etimedout/i,
94
+ /enotfound/i,
95
+ /network/i,
96
+ /dns/i,
97
+ ],
98
+ messageMatchers: [
99
+ /connection.*refused/i,
100
+ /connection.*reset/i,
101
+ /timed.?out/i,
102
+ /getaddrinfo/i,
103
+ /network/i,
104
+ ],
105
+ statusCodes: [],
106
+ },
107
+ {
108
+ class: FailureClass.BROWSER_RUNTIME,
109
+ errorCodeMatchers: [
110
+ /browser/i,
111
+ /playwright/i,
112
+ /puppeteer/i,
113
+ /chromium/i,
114
+ /navigation.?timeout/i,
115
+ /page.?crash/i,
116
+ ],
117
+ messageMatchers: [
118
+ /browser.*crash/i,
119
+ /page.*crash/i,
120
+ /navigation.*timeout/i,
121
+ /context.*destroyed/i,
122
+ ],
123
+ statusCodes: [],
124
+ },
125
+ {
126
+ class: FailureClass.CONFIGURATION,
127
+ errorCodeMatchers: [/config/i, /option/i],
128
+ messageMatchers: [/invalid.*config/i, /missing.*option/i, /bad.*parameter/i],
129
+ statusCodes: [],
130
+ },
131
+ ];
132
+
133
+ /**
134
+ * Classify a raw observation into a FailureClass.
135
+ * Pure function: deterministic given the same input shape.
136
+ *
137
+ * @param {Object} observation - shape: { errorCode?, message?, statusCode?, name? }
138
+ * @returns {string} FailureClass
139
+ */
140
+ export function classifyObservation(observation) {
141
+ if (!observation || typeof observation !== "object") {
142
+ return FailureClass.UNKNOWN;
143
+ }
144
+
145
+ const errorCode = String(observation.errorCode ?? "");
146
+ const message = String(observation.message ?? "");
147
+ const name = String(observation.name ?? "");
148
+ const statusCode =
149
+ typeof observation.statusCode === "number" ? observation.statusCode : null;
150
+
151
+ const haystack = `${errorCode}\n${message}\n${name}`;
152
+
153
+ // Status-code-first: anti-bot 403 wins over a generic browser error
154
+ if (statusCode !== null) {
155
+ const statusHit = CLASSIFICATION_RULES.find(
156
+ (r) => r.statusCodes.includes(statusCode),
157
+ );
158
+ if (statusHit) return statusHit.class;
159
+ }
160
+
161
+ // Pattern matchers — order matters (most specific first)
162
+ for (const rule of CLASSIFICATION_RULES) {
163
+ const codeHit = rule.errorCodeMatchers.some((rx) => rx.test(haystack));
164
+ const msgHit = rule.messageMatchers.some((rx) => rx.test(haystack));
165
+ if (codeHit || msgHit) return rule.class;
166
+ }
167
+
168
+ return FailureClass.UNKNOWN;
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // Candidate cause generation
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * @typedef {Object} CandidateCause
177
+ * @property {string} id
178
+ * @property {string} failureClass
179
+ * @property {string} label
180
+ * @property {string} description
181
+ * @property {number} prior - base plausibility 0..1
182
+ */
183
+
184
+ /**
185
+ * Generate plausible candidate causes for a failure class.
186
+ *
187
+ * @param {string} failureClass
188
+ * @param {Object} [observation]
189
+ * @returns {CandidateCause[]}
190
+ */
191
+ export function generateCandidateCauses(failureClass, observation = {}) {
192
+ switch (failureClass) {
193
+ case FailureClass.MISSING_EXECUTABLE:
194
+ return [
195
+ {
196
+ id: "MISSING_EXECUTABLE",
197
+ failureClass,
198
+ label: "Browser executable not installed",
199
+ description:
200
+ "Playwright/Puppeteer binary is missing from the host (e.g. Chromium not downloaded).",
201
+ prior: 0.9,
202
+ },
203
+ {
204
+ id: "WRONG_EXECUTABLE_PATH",
205
+ failureClass,
206
+ label: "Browser executable path misconfigured",
207
+ description: "The configured executablePath points to a non-existent binary.",
208
+ prior: 0.2,
209
+ },
210
+ ];
211
+
212
+ case FailureClass.BROWSER_RUNTIME:
213
+ return [
214
+ {
215
+ id: "LAUNCH_FAILED",
216
+ failureClass,
217
+ label: "Browser launch failed",
218
+ description: "Browser process failed to start (missing deps, sandboxing, etc.).",
219
+ prior: 0.7,
220
+ },
221
+ {
222
+ id: "PAGE_CRASH",
223
+ failureClass,
224
+ label: "Page crashed during operation",
225
+ description: "The page or tab crashed mid-operation (OOM, renderer kill).",
226
+ prior: 0.3,
227
+ },
228
+ {
229
+ id: "NAVIGATION_TIMEOUT",
230
+ failureClass,
231
+ label: "Navigation timeout",
232
+ description: "Page took too long to load (network or heavy SPA).",
233
+ prior: 0.3,
234
+ },
235
+ ];
236
+
237
+ case FailureClass.ANTI_BOT:
238
+ return [
239
+ {
240
+ id: "WAF_BLOCK",
241
+ failureClass,
242
+ label: "WAF / Cloudflare block",
243
+ description: "Edge WAF detected the request fingerprint and blocked it (403/captcha).",
244
+ prior: 0.7,
245
+ },
246
+ {
247
+ id: "GEO_BLOCK",
248
+ failureClass,
249
+ label: "Geo / IP block",
250
+ description: "The site is restricting access based on IP geography.",
251
+ prior: 0.2,
252
+ },
253
+ ];
254
+
255
+ case FailureClass.NETWORK_ERROR:
256
+ return [
257
+ {
258
+ id: "DNS_FAILURE",
259
+ failureClass,
260
+ label: "DNS resolution failure",
261
+ description: "Hostname could not be resolved.",
262
+ prior: 0.4,
263
+ },
264
+ {
265
+ id: "CONNECTION_REFUSED",
266
+ failureClass,
267
+ label: "Connection refused by remote",
268
+ description: "TCP connection actively refused (service down or filtered).",
269
+ prior: 0.4,
270
+ },
271
+ {
272
+ id: "TLS_ERROR",
273
+ failureClass,
274
+ label: "TLS / certificate error",
275
+ description: "TLS handshake or certificate validation failed.",
276
+ prior: 0.2,
277
+ },
278
+ ];
279
+
280
+ case FailureClass.AUTH:
281
+ return [
282
+ {
283
+ id: "INVALID_CREDENTIALS",
284
+ failureClass,
285
+ label: "Invalid credentials",
286
+ description: "Provided credentials/token are rejected by the server.",
287
+ prior: 0.8,
288
+ },
289
+ {
290
+ id: "EXPIRED_TOKEN",
291
+ failureClass,
292
+ label: "Expired token",
293
+ description: "Authentication token has expired and needs refresh.",
294
+ prior: 0.4,
295
+ },
296
+ ];
297
+
298
+ case FailureClass.RATE_LIMIT:
299
+ return [
300
+ {
301
+ id: "REQUESTS_THROTTLED",
302
+ failureClass,
303
+ label: "Requests throttled",
304
+ description: "Server is throttling requests; backoff is required.",
305
+ prior: 0.9,
306
+ },
307
+ ];
308
+
309
+ case FailureClass.CONFIGURATION:
310
+ return [
311
+ {
312
+ id: "BAD_CONFIG",
313
+ failureClass,
314
+ label: "Invalid configuration value",
315
+ description: "One or more required configuration values are invalid.",
316
+ prior: 0.9,
317
+ },
318
+ ];
319
+
320
+ case FailureClass.UNKNOWN:
321
+ default:
322
+ return [
323
+ {
324
+ id: "UNCLASSIFIED",
325
+ failureClass,
326
+ label: "Unclassified failure",
327
+ description: "Failure does not match any known pattern; further investigation needed.",
328
+ prior: 1.0,
329
+ },
330
+ ];
331
+ }
332
+ }
333
+
334
+ // ─────────────────────────────────────────────────────────────────────────────
335
+ // Discriminating experiment derivation
336
+ // ─────────────────────────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * @typedef {Object} Experiment
340
+ * @property {string} kind - "shell" | "http" | "browser-probe" | "fs-probe"
341
+ * @property {string} candidateId
342
+ * @property {string} command - shell command / HTTP URL / probe path
343
+ * @property {Object} expectedObservation - shape passed to assessCause
344
+ * @property {string} description
345
+ */
346
+
347
+ /**
348
+ * Derive the MINIMAL experiment that would discriminate a candidate cause.
349
+ * Experiments are deliberately side-effect-free probes.
350
+ *
351
+ * @param {CandidateCause} candidate
352
+ * @returns {Experiment}
353
+ */
354
+ export function deriveDiscriminatingExperiment(candidate) {
355
+ switch (candidate.id) {
356
+ case "MISSING_EXECUTABLE":
357
+ return {
358
+ kind: "shell",
359
+ candidateId: candidate.id,
360
+ command: "command -v chromium || command -v google-chrome || npx playwright --version",
361
+ expectedObservation: {
362
+ errorCode: "PRESENT",
363
+ message: "browser executable resolvable",
364
+ statusCode: 200,
365
+ },
366
+ description: "Verify a browser executable is reachable on PATH or installed via Playwright.",
367
+ };
368
+
369
+ case "WRONG_EXECUTABLE_PATH":
370
+ return {
371
+ kind: "fs-probe",
372
+ candidateId: candidate.id,
373
+ command: "stat -c '%F' ${EXECUTABLE_PATH:-/nonexistent}",
374
+ expectedObservation: { errorCode: "ENOENT", message: "No such file" },
375
+ description: "Stat the configured executablePath; expect ENOENT if misconfigured.",
376
+ };
377
+
378
+ case "LAUNCH_FAILED":
379
+ return {
380
+ kind: "shell",
381
+ candidateId: candidate.id,
382
+ command: "node -e \"require('playwright').chromium.launch().then(b=>b.close())\"",
383
+ expectedObservation: { errorCode: "OK", message: "launch succeeded" },
384
+ description: "Attempt a minimal headless launch to isolate the failure.",
385
+ };
386
+
387
+ case "PAGE_CRASH":
388
+ return {
389
+ kind: "browser-probe",
390
+ candidateId: candidate.id,
391
+ command: "goto about:blank",
392
+ expectedObservation: { errorCode: "OK", message: "about:blank loaded" },
393
+ description: "Load a trivial page; if it crashes here, browser itself is unstable.",
394
+ };
395
+
396
+ case "NAVIGATION_TIMEOUT":
397
+ return {
398
+ kind: "http",
399
+ candidateId: candidate.id,
400
+ command: "GET /",
401
+ expectedObservation: { statusCode: 200, message: "ok" },
402
+ description: "Hit the target URL via plain HTTP to confirm reachability independent of JS.",
403
+ };
404
+
405
+ case "WAF_BLOCK":
406
+ case "GEO_BLOCK":
407
+ return {
408
+ kind: "http",
409
+ candidateId: candidate.id,
410
+ command: "GET / -H 'User-Agent: curl/8.0'",
411
+ expectedObservation: { statusCode: 200, message: "ok" },
412
+ description:
413
+ "Probe with a neutral UA; if 200, the prior block was fingerprint-based.",
414
+ };
415
+
416
+ case "DNS_FAILURE":
417
+ return {
418
+ kind: "shell",
419
+ candidateId: candidate.id,
420
+ command: "getent hosts example.com",
421
+ expectedObservation: { errorCode: "OK", message: "host resolves" },
422
+ description: "Resolve DNS for the target hostname.",
423
+ };
424
+
425
+ case "CONNECTION_REFUSED":
426
+ return {
427
+ kind: "shell",
428
+ candidateId: candidate.id,
429
+ command: "nc -zv -w 2 example.com 443",
430
+ expectedObservation: { errorCode: "OK", message: "open" },
431
+ description: "TCP probe to verify the port is open from this host.",
432
+ };
433
+
434
+ case "TLS_ERROR":
435
+ return {
436
+ kind: "shell",
437
+ candidateId: candidate.id,
438
+ command: "openssl s_client -connect example.com:443 -servername example.com </dev/null",
439
+ expectedObservation: { errorCode: "OK", message: "verify return:1" },
440
+ description: "TLS handshake probe with certificate verification.",
441
+ };
442
+
443
+ case "INVALID_CREDENTIALS":
444
+ case "EXPIRED_TOKEN":
445
+ return {
446
+ kind: "http",
447
+ candidateId: candidate.id,
448
+ command: "GET /api/me -H 'Authorization: Bearer ${TOKEN}'",
449
+ expectedObservation: { statusCode: 200, message: "ok" },
450
+ description: "Hit a known authenticated endpoint to validate the token.",
451
+ };
452
+
453
+ case "REQUESTS_THROTTLED":
454
+ return {
455
+ kind: "shell",
456
+ candidateId: candidate.id,
457
+ command: "sleep 30 && curl -sS -o /dev/null -w '%{http_code}' https://example.com/",
458
+ expectedObservation: { statusCode: 200, message: "ok" },
459
+ description: "Wait 30s and retry once; if it succeeds, throttling was the cause.",
460
+ };
461
+
462
+ case "BAD_CONFIG":
463
+ return {
464
+ kind: "fs-probe",
465
+ candidateId: candidate.id,
466
+ command: "cat ${CONFIG_PATH}",
467
+ expectedObservation: { errorCode: "OK", message: "config loaded" },
468
+ description: "Read and validate the configuration file.",
469
+ };
470
+
471
+ case "UNCLASSIFIED":
472
+ default:
473
+ return {
474
+ kind: "shell",
475
+ candidateId: candidate.id,
476
+ command: "echo 'no discriminating experiment defined'",
477
+ expectedObservation: { errorCode: "OK", message: "noop" },
478
+ description: "No targeted experiment available; manual investigation required.",
479
+ };
480
+ }
481
+ }
482
+
483
+ // ─────────────────────────────────────────────────────────────────────────────
484
+ // Cause assessment
485
+ // ─────────────────────────────────────────────────────────────────────────────
486
+
487
+ /**
488
+ * Assess whether an observed experiment result supports, contradicts, or is
489
+ * inconclusive with respect to a candidate cause.
490
+ *
491
+ * Decision logic:
492
+ * - If observation is missing/null → INCONCLUSIVE
493
+ * - If observation.errorCode / statusCode matches
494
+ * experiment.expectedObservation → SUPPORTED
495
+ * - If observation is the OPPOSITE of expected → CONTRADICTED
496
+ * - Otherwise → INCONCLUSIVE
497
+ *
498
+ * @param {CandidateCause} candidate
499
+ * @param {Experiment} experiment
500
+ * @param {Object|null} observationResult
501
+ * @returns {{result: string, reason: string}}
502
+ */
503
+ export function assessCause(candidate, experiment, observationResult) {
504
+ if (observationResult === null || observationResult === undefined) {
505
+ return {
506
+ result: AssessmentResult.INCONCLUSIVE,
507
+ reason: "no observation collected yet",
508
+ };
509
+ }
510
+
511
+ const expected = experiment?.expectedObservation ?? {};
512
+ const observed = observationResult ?? {};
513
+
514
+ // Build two flat haystacks — one from expected, one from observed — and ask
515
+ // whether they overlap. We deliberately keep this lightweight: a real system
516
+ // would call createAssessment on the full object.
517
+ const expSignals = collectSignals(expected);
518
+ const obsSignals = collectSignals(observed);
519
+
520
+ const overlap = expSignals.filter((s) => obsSignals.includes(s));
521
+ const inverse = expSignals
522
+ .map((s) => inverseSignal(s))
523
+ .filter((s) => obsSignals.includes(s));
524
+
525
+ if (overlap.length > 0) {
526
+ return {
527
+ result: AssessmentResult.SUPPORTED,
528
+ reason: `observation matches expected signal(s): ${overlap.join(", ")}`,
529
+ };
530
+ }
531
+
532
+ if (inverse.length > 0) {
533
+ return {
534
+ result: AssessmentResult.CONTRADICTED,
535
+ reason: `observation contradicts expected signal(s): ${inverse.join(", ")}`,
536
+ };
537
+ }
538
+
539
+ return {
540
+ result: AssessmentResult.INCONCLUSIVE,
541
+ reason: "observation has no overlapping or inverse signal with expectation",
542
+ };
543
+ }
544
+
545
+ function collectSignals(obj) {
546
+ const out = [];
547
+ for (const [k, v] of Object.entries(obj ?? {})) {
548
+ if (v === undefined || v === null) continue;
549
+ out.push(`${k}:${String(v)}`);
550
+ }
551
+ return out;
552
+ }
553
+
554
+ function inverseSignal(signal) {
555
+ const [k, v] = signal.split(":");
556
+ if (k === "errorCode" && v === "ENOENT") return "errorCode:PRESENT";
557
+ if (k === "errorCode" && v === "PRESENT") return "errorCode:ENOENT";
558
+ if (k === "statusCode") {
559
+ if (v === "200") return "statusCode:403";
560
+ if (v === "403") return "statusCode:200";
561
+ if (v === "401") return "statusCode:200";
562
+ }
563
+ return `__inverse_unknown__:${signal}`;
564
+ }
565
+
566
+ // ─────────────────────────────────────────────────────────────────────────────
567
+ // Orchestrator
568
+ // ─────────────────────────────────────────────────────────────────────────────
569
+
570
+ /**
571
+ * @typedef {Object} DiagnosisResult
572
+ * @property {string} failureClass
573
+ * @property {CandidateCause[]} candidates
574
+ * @property {Experiment|null} recommendedExperiment
575
+ * @property {string} summary
576
+ */
577
+
578
+ /**
579
+ * Run one iteration of the diagnostic causal loop.
580
+ *
581
+ * @param {Object} args
582
+ * @param {Object} args.observation - raw failure observation
583
+ * @param {Object} [args.previousDiagnosis] - prior diagnosis to reuse / refine
584
+ * @returns {DiagnosisResult}
585
+ */
586
+ export function diagnose({ observation, previousDiagnosis = null } = {}) {
587
+ const failureClass = classifyObservation(observation);
588
+ const candidates = generateCandidateCauses(failureClass, observation);
589
+
590
+ // Re-rank candidates: prefer the most specific prior if available.
591
+ const ranked = rankCandidates(candidates, previousDiagnosis);
592
+
593
+ const recommendedExperiment = ranked.length > 0
594
+ ? deriveDiscriminatingExperiment(ranked[0])
595
+ : null;
596
+
597
+ const summary =
598
+ `failure=${failureClass} candidates=${ranked.length} top=${ranked[0]?.id ?? "none"}`;
599
+
600
+ return {
601
+ failureClass,
602
+ candidates: ranked,
603
+ recommendedExperiment,
604
+ summary,
605
+ };
606
+ }
607
+
608
+ function rankCandidates(candidates, previousDiagnosis) {
609
+ if (!previousDiagnosis || !Array.isArray(previousDiagnosis.candidates)) {
610
+ return [...candidates].sort((a, b) => b.prior - a.prior);
611
+ }
612
+
613
+ const priorById = new Map(
614
+ previousDiagnosis.candidates.map((c) => [c.id, c.prior ?? 0]),
615
+ );
616
+
617
+ return [...candidates]
618
+ .map((c) => ({
619
+ ...c,
620
+ prior: priorById.has(c.id)
621
+ ? (c.prior ?? 0) + (priorById.get(c.id) ?? 0)
622
+ : c.prior ?? 0,
623
+ }))
624
+ .sort((a, b) => b.prior - a.prior);
625
+ }
626
+
627
+ // ─────────────────────────────────────────────────────────────────────────────
628
+ // Convenience export: anti-bot heuristic guard (used by golden test)
629
+ // ─────────────────────────────────────────────────────────────────────────────
630
+
631
+ /**
632
+ * Determine whether anti-bot / stealth measures are SUPPORTED for a given
633
+ * diagnosis. The rule: ANTI_BOT is only supported when at least one candidate
634
+ * has the WAF_BLOCK / GEO_BLOCK class AND the failure class itself is ANTI_BOT.
635
+ *
636
+ * If the failure class is BROWSER_RUNTIME / MISSING_EXECUTABLE, anti-bot is
637
+ * explicitly NOT supported (the agent must NOT assume stealth).
638
+ *
639
+ * @param {DiagnosisResult} diagnosis
640
+ * @returns {"SUPPORTED" | "NOT_SUPPORTED" | "INCONCLUSIVE"}
641
+ */
642
+ export function antiBotSupport(diagnosis) {
643
+ if (!diagnosis || typeof diagnosis !== "object") return "INCONCLUSIVE";
644
+
645
+ if (diagnosis.failureClass === FailureClass.ANTI_BOT) {
646
+ return "SUPPORTED";
647
+ }
648
+
649
+ // If the actual evidence is browser-runtime (e.g. missing executable), the
650
+ // agent must NOT auto-assume stealth/anti-bot measures are needed.
651
+ if (
652
+ diagnosis.failureClass === FailureClass.BROWSER_RUNTIME ||
653
+ diagnosis.failureClass === FailureClass.MISSING_EXECUTABLE
654
+ ) {
655
+ return "NOT_SUPPORTED";
656
+ }
657
+
658
+ return "INCONCLUSIVE";
659
+ }