opencode-bridge 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/PKG-INFO +1 -1
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/opencode_bridge/server.py +37 -566
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/pyproject.toml +1 -1
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/skills/opencode.md +14 -28
- opencode_bridge-0.3.0/tests/test_companion.py +114 -0
- opencode_bridge-0.2.0/tests/test_companion.py +0 -267
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/.claude-plugin/plugin.json +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/.github/workflows/ci.yml +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/.github/workflows/release.yml +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/.gitignore +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/README.md +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/opencode_bridge/__init__.py +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/opencode_bridge/install.py +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/tests/__init__.py +0 -0
- {opencode_bridge-0.2.0 → opencode_bridge-0.3.0}/uv.lock +0 -0
|
@@ -214,546 +214,30 @@ def build_message_prompt(message: str, file_paths: list[str]) -> str:
|
|
|
214
214
|
|
|
215
215
|
|
|
216
216
|
# ---------------------------------------------------------------------------
|
|
217
|
-
#
|
|
217
|
+
# Companion System — Auto-Framing
|
|
218
218
|
# ---------------------------------------------------------------------------
|
|
219
219
|
|
|
220
|
-
@dataclass
|
|
221
|
-
class DomainProfile:
|
|
222
|
-
"""Defines a domain of expertise with persona, frameworks, and approach."""
|
|
223
|
-
id: str
|
|
224
|
-
name: str
|
|
225
|
-
keywords: list[str]
|
|
226
|
-
phrases: list[str]
|
|
227
|
-
file_indicators: list[str] # file extensions or name patterns
|
|
228
|
-
expert_persona: str
|
|
229
|
-
thinking_frameworks: list[str]
|
|
230
|
-
key_questions: list[str]
|
|
231
|
-
structured_approach: list[str]
|
|
232
|
-
agent_hint: str # suggested opencode agent
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
DOMAIN_REGISTRY: dict[str, DomainProfile] = {}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _register(*profiles: DomainProfile):
|
|
239
|
-
for p in profiles:
|
|
240
|
-
DOMAIN_REGISTRY[p.id] = p
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
_register(
|
|
244
|
-
DomainProfile(
|
|
245
|
-
id="architecture",
|
|
246
|
-
name="Architecture & System Design",
|
|
247
|
-
keywords=["architecture", "microservice", "monolith", "scalab", "distributed",
|
|
248
|
-
"component", "module", "layer", "decouple", "coupling", "cohesion",
|
|
249
|
-
"event", "queue", "broker", "gateway", "proxy", "load balancer"],
|
|
250
|
-
phrases=["system design", "event driven", "event sourcing", "service mesh",
|
|
251
|
-
"domain driven", "hexagonal architecture", "clean architecture",
|
|
252
|
-
"micro frontend", "message bus", "data pipeline", "cqrs"],
|
|
253
|
-
file_indicators=[".proto", ".yaml", ".yml", ".tf", ".hcl"],
|
|
254
|
-
expert_persona=(
|
|
255
|
-
"a senior distributed systems architect who has designed systems serving "
|
|
256
|
-
"millions of users. You think in terms of components, boundaries, data flow, "
|
|
257
|
-
"and failure modes. You've seen both over-engineered and under-engineered "
|
|
258
|
-
"systems and know when each approach is appropriate."
|
|
259
|
-
),
|
|
260
|
-
thinking_frameworks=["C4 model (context, containers, components, code)",
|
|
261
|
-
"CAP theorem", "DDD (bounded contexts, aggregates)",
|
|
262
|
-
"CQRS/Event Sourcing trade-offs",
|
|
263
|
-
"Twelve-Factor App principles"],
|
|
264
|
-
key_questions=["What are the key quality attributes (latency, throughput, availability)?",
|
|
265
|
-
"Where are the domain boundaries?",
|
|
266
|
-
"What data consistency model fits here?",
|
|
267
|
-
"What happens when a component fails?",
|
|
268
|
-
"How will this evolve in 6-12 months?"],
|
|
269
|
-
structured_approach=["Clarify requirements and constraints",
|
|
270
|
-
"Identify components and their responsibilities",
|
|
271
|
-
"Define interfaces and data flow",
|
|
272
|
-
"Analyze trade-offs and failure modes",
|
|
273
|
-
"Recommend with rationale"],
|
|
274
|
-
agent_hint="plan",
|
|
275
|
-
),
|
|
276
|
-
DomainProfile(
|
|
277
|
-
id="debugging",
|
|
278
|
-
name="Debugging & Troubleshooting",
|
|
279
|
-
keywords=["bug", "error", "crash", "fail", "exception", "traceback",
|
|
280
|
-
"stacktrace", "debug", "breakpoint", "segfault", "panic",
|
|
281
|
-
"hang", "freeze", "corrupt", "unexpected", "wrong"],
|
|
282
|
-
phrases=["root cause", "stack trace", "doesn't work", "stopped working",
|
|
283
|
-
"race condition", "deadlock", "memory leak", "null pointer",
|
|
284
|
-
"off by one", "regression", "flaky test", "intermittent failure"],
|
|
285
|
-
file_indicators=[".log", ".dump", ".core"],
|
|
286
|
-
expert_persona=(
|
|
287
|
-
"a seasoned debugger who has tracked down the most elusive bugs — race "
|
|
288
|
-
"conditions, heisenbugs, memory corruption, off-by-one errors hidden for "
|
|
289
|
-
"years. You are methodical, hypothesis-driven, and never jump to conclusions."
|
|
290
|
-
),
|
|
291
|
-
thinking_frameworks=["Five Whys (root cause analysis)",
|
|
292
|
-
"Scientific method (hypothesize, test, refine)",
|
|
293
|
-
"Binary search / bisection for isolating changes",
|
|
294
|
-
"Rubber duck debugging"],
|
|
295
|
-
key_questions=["When did it start happening? What changed?",
|
|
296
|
-
"Is it reproducible? Under what conditions?",
|
|
297
|
-
"What are the exact symptoms vs. expected behavior?",
|
|
298
|
-
"Have we ruled out environment differences?",
|
|
299
|
-
"What is the minimal reproduction case?"],
|
|
300
|
-
structured_approach=["Reproduce and isolate the issue",
|
|
301
|
-
"Form hypotheses ranked by likelihood",
|
|
302
|
-
"Gather evidence: logs, traces, state inspection",
|
|
303
|
-
"Narrow down via elimination",
|
|
304
|
-
"Fix, verify, and prevent regression"],
|
|
305
|
-
agent_hint="build",
|
|
306
|
-
),
|
|
307
|
-
DomainProfile(
|
|
308
|
-
id="performance",
|
|
309
|
-
name="Performance & Optimization",
|
|
310
|
-
keywords=["performance", "optimize", "bottleneck", "latency", "throughput",
|
|
311
|
-
"cache", "profil", "benchmark", "slow", "fast", "speed",
|
|
312
|
-
"memory", "cpu", "io", "bandwidth", "concurren"],
|
|
313
|
-
phrases=["cache miss", "hot path", "time complexity", "space complexity",
|
|
314
|
-
"p99 latency", "tail latency", "garbage collection", "connection pool",
|
|
315
|
-
"query plan", "flame graph", "load test"],
|
|
316
|
-
file_indicators=[".perf", ".prof", ".bench"],
|
|
317
|
-
expert_persona=(
|
|
318
|
-
"a performance engineer who obsesses over microseconds and memory allocations. "
|
|
319
|
-
"You profile before optimizing, know that premature optimization is the root of "
|
|
320
|
-
"all evil, and always ask 'what does the data say?' before recommending changes."
|
|
321
|
-
),
|
|
322
|
-
thinking_frameworks=["Amdahl's Law", "Little's Law",
|
|
323
|
-
"USE method (Utilization, Saturation, Errors)",
|
|
324
|
-
"Roofline model", "Big-O analysis with practical constants"],
|
|
325
|
-
key_questions=["What is the actual bottleneck (CPU, memory, I/O, network)?",
|
|
326
|
-
"Do we have profiling data or benchmarks?",
|
|
327
|
-
"What's the target performance? Current baseline?",
|
|
328
|
-
"What are the hot paths?",
|
|
329
|
-
"What trade-offs are acceptable (memory vs speed, complexity vs perf)?"],
|
|
330
|
-
structured_approach=["Measure current performance with profiling/benchmarks",
|
|
331
|
-
"Identify the bottleneck — do not guess",
|
|
332
|
-
"Propose targeted optimizations",
|
|
333
|
-
"Estimate impact and trade-offs",
|
|
334
|
-
"Measure again after changes"],
|
|
335
|
-
agent_hint="build",
|
|
336
|
-
),
|
|
337
|
-
DomainProfile(
|
|
338
|
-
id="security",
|
|
339
|
-
name="Security & Threat Modeling",
|
|
340
|
-
keywords=["security", "vulnerab", "auth", "token", "encrypt", "hash",
|
|
341
|
-
"ssl", "tls", "cors", "csrf", "xss", "injection", "sanitiz",
|
|
342
|
-
"permission", "privilege", "secret", "credential"],
|
|
343
|
-
phrases=["sql injection", "cross site", "threat model", "attack surface",
|
|
344
|
-
"zero trust", "defense in depth", "least privilege",
|
|
345
|
-
"owasp top 10", "security audit", "penetration test",
|
|
346
|
-
"access control", "input validation"],
|
|
347
|
-
file_indicators=[".pem", ".key", ".cert", ".env"],
|
|
348
|
-
expert_persona=(
|
|
349
|
-
"a senior application security engineer who thinks like an attacker but "
|
|
350
|
-
"builds like a defender. You know the OWASP Top 10 by heart, understand "
|
|
351
|
-
"cryptographic primitives, and always consider the full threat model."
|
|
352
|
-
),
|
|
353
|
-
thinking_frameworks=["STRIDE threat modeling",
|
|
354
|
-
"OWASP Top 10",
|
|
355
|
-
"Defense in depth",
|
|
356
|
-
"Zero trust architecture",
|
|
357
|
-
"Principle of least privilege"],
|
|
358
|
-
key_questions=["What is the threat model? Who are the adversaries?",
|
|
359
|
-
"What data is sensitive and how is it protected?",
|
|
360
|
-
"Where are the trust boundaries?",
|
|
361
|
-
"What authentication and authorization model is in use?",
|
|
362
|
-
"Are there known CVEs in dependencies?"],
|
|
363
|
-
structured_approach=["Identify assets and threat actors",
|
|
364
|
-
"Map the attack surface",
|
|
365
|
-
"Enumerate threats (STRIDE)",
|
|
366
|
-
"Assess risk (likelihood x impact)",
|
|
367
|
-
"Recommend mitigations prioritized by risk"],
|
|
368
|
-
agent_hint="plan",
|
|
369
|
-
),
|
|
370
|
-
DomainProfile(
|
|
371
|
-
id="testing",
|
|
372
|
-
name="Testing & Quality Assurance",
|
|
373
|
-
keywords=["test", "assert", "mock", "stub", "fixture", "coverage",
|
|
374
|
-
"spec", "suite", "expect", "verify", "tdd", "bdd"],
|
|
375
|
-
phrases=["unit test", "integration test", "end to end", "test coverage",
|
|
376
|
-
"test driven", "edge case", "boundary condition", "test pyramid",
|
|
377
|
-
"property based", "mutation testing", "snapshot test",
|
|
378
|
-
"regression test"],
|
|
379
|
-
file_indicators=["_test.py", "_test.go", ".test.js", ".test.ts", ".spec.js",
|
|
380
|
-
".spec.ts", "_spec.rb"],
|
|
381
|
-
expert_persona=(
|
|
382
|
-
"a testing specialist who believes tests are living documentation. You "
|
|
383
|
-
"understand the test pyramid, know when to mock and when not to, and "
|
|
384
|
-
"write tests that catch real bugs without being brittle."
|
|
385
|
-
),
|
|
386
|
-
thinking_frameworks=["Test pyramid (unit → integration → e2e)",
|
|
387
|
-
"FIRST principles (Fast, Independent, Repeatable, Self-validating, Timely)",
|
|
388
|
-
"Arrange-Act-Assert pattern",
|
|
389
|
-
"Equivalence partitioning & boundary value analysis"],
|
|
390
|
-
key_questions=["What behavior are we verifying?",
|
|
391
|
-
"What are the edge cases and boundary conditions?",
|
|
392
|
-
"Is this a unit, integration, or e2e concern?",
|
|
393
|
-
"What should we mock vs. use real implementations?",
|
|
394
|
-
"How will we know if this test is catching real bugs?"],
|
|
395
|
-
structured_approach=["Identify what behavior to test",
|
|
396
|
-
"Determine test level (unit/integration/e2e)",
|
|
397
|
-
"Design test cases covering happy path and edge cases",
|
|
398
|
-
"Write clear, maintainable assertions",
|
|
399
|
-
"Review for brittleness and false confidence"],
|
|
400
|
-
agent_hint="build",
|
|
401
|
-
),
|
|
402
|
-
DomainProfile(
|
|
403
|
-
id="devops",
|
|
404
|
-
name="DevOps & Infrastructure",
|
|
405
|
-
keywords=["deploy", "pipeline", "container", "docker", "kubernetes", "k8s",
|
|
406
|
-
"terraform", "ansible", "helm", "ci", "cd", "infra", "cloud",
|
|
407
|
-
"aws", "gcp", "azure", "monitoring", "alert", "observ"],
|
|
408
|
-
phrases=["ci/cd pipeline", "infrastructure as code", "blue green deployment",
|
|
409
|
-
"canary release", "rolling update", "auto scaling",
|
|
410
|
-
"service discovery", "container orchestration",
|
|
411
|
-
"gitops", "platform engineering"],
|
|
412
|
-
file_indicators=[".tf", ".hcl", "Dockerfile", ".yml", ".yaml",
|
|
413
|
-
"Jenkinsfile", ".github"],
|
|
414
|
-
expert_persona=(
|
|
415
|
-
"a senior DevOps/platform engineer who has managed production infrastructure "
|
|
416
|
-
"at scale. You think in terms of reliability, repeatability, and observability. "
|
|
417
|
-
"You know that every manual step is a future incident."
|
|
418
|
-
),
|
|
419
|
-
thinking_frameworks=["DORA metrics (deployment frequency, lead time, MTTR, change failure rate)",
|
|
420
|
-
"Infrastructure as Code principles",
|
|
421
|
-
"SRE golden signals (latency, traffic, errors, saturation)",
|
|
422
|
-
"GitOps workflow"],
|
|
423
|
-
key_questions=["What is the deployment target (cloud, on-prem, hybrid)?",
|
|
424
|
-
"What are the reliability requirements (SLOs)?",
|
|
425
|
-
"How do we roll back if something goes wrong?",
|
|
426
|
-
"What observability do we have?",
|
|
427
|
-
"What is the blast radius of a bad deploy?"],
|
|
428
|
-
structured_approach=["Assess current infrastructure and deployment process",
|
|
429
|
-
"Identify gaps in reliability and automation",
|
|
430
|
-
"Design pipeline and infrastructure changes",
|
|
431
|
-
"Plan rollout with rollback strategy",
|
|
432
|
-
"Define success metrics and alerts"],
|
|
433
|
-
agent_hint="plan",
|
|
434
|
-
),
|
|
435
|
-
DomainProfile(
|
|
436
|
-
id="database",
|
|
437
|
-
name="Database & Data Modeling",
|
|
438
|
-
keywords=["database", "schema", "table", "column", "index", "query",
|
|
439
|
-
"sql", "nosql", "migration", "join", "foreign key", "primary key",
|
|
440
|
-
"transaction", "acid", "normali", "partition", "shard", "replica"],
|
|
441
|
-
phrases=["query optimization", "execution plan", "database migration",
|
|
442
|
-
"data model", "schema design", "query plan", "n+1 query",
|
|
443
|
-
"connection pool", "read replica", "write ahead log",
|
|
444
|
-
"eventual consistency"],
|
|
445
|
-
file_indicators=[".sql", ".prisma", ".migration"],
|
|
446
|
-
expert_persona=(
|
|
447
|
-
"a database architect with deep expertise in both relational and NoSQL systems. "
|
|
448
|
-
"You think about data access patterns first, schema second. You've tuned queries "
|
|
449
|
-
"from minutes to milliseconds and know when denormalization is the right call."
|
|
450
|
-
),
|
|
451
|
-
thinking_frameworks=["Normal forms (1NF through BCNF) and when to denormalize",
|
|
452
|
-
"ACID vs BASE trade-offs",
|
|
453
|
-
"Index design (B-tree, hash, composite, covering)",
|
|
454
|
-
"CAP theorem applied to data stores"],
|
|
455
|
-
key_questions=["What are the primary access patterns (reads vs writes)?",
|
|
456
|
-
"What consistency guarantees are needed?",
|
|
457
|
-
"How much data and what growth rate?",
|
|
458
|
-
"What are the query performance requirements?",
|
|
459
|
-
"How will the schema evolve?"],
|
|
460
|
-
structured_approach=["Understand access patterns and data relationships",
|
|
461
|
-
"Design schema to match access patterns",
|
|
462
|
-
"Plan indexing strategy",
|
|
463
|
-
"Consider partitioning/sharding needs",
|
|
464
|
-
"Design migration path from current state"],
|
|
465
|
-
agent_hint="build",
|
|
466
|
-
),
|
|
467
|
-
DomainProfile(
|
|
468
|
-
id="api_design",
|
|
469
|
-
name="API Design",
|
|
470
|
-
keywords=["api", "endpoint", "rest", "graphql", "grpc", "webhook",
|
|
471
|
-
"pagination", "versioning", "rate limit", "openapi", "swagger",
|
|
472
|
-
"request", "response", "payload", "header", "status code"],
|
|
473
|
-
phrases=["rest api", "api design", "api versioning", "breaking change",
|
|
474
|
-
"backward compatible", "content negotiation", "hateoas",
|
|
475
|
-
"api gateway", "graphql schema", "api contract"],
|
|
476
|
-
file_indicators=[".openapi", ".swagger", ".graphql", ".gql", ".proto"],
|
|
477
|
-
expert_persona=(
|
|
478
|
-
"a senior API designer who has built APIs used by thousands of developers. "
|
|
479
|
-
"You think about developer experience, consistency, evolvability, and "
|
|
480
|
-
"backward compatibility. You know REST deeply but aren't dogmatic about it."
|
|
481
|
-
),
|
|
482
|
-
thinking_frameworks=["REST maturity model (Richardson)",
|
|
483
|
-
"API-first design",
|
|
484
|
-
"Consumer-driven contracts",
|
|
485
|
-
"Robustness principle (be liberal in what you accept)"],
|
|
486
|
-
key_questions=["Who are the API consumers (internal, external, both)?",
|
|
487
|
-
"What operations does the API need to support?",
|
|
488
|
-
"How will we handle versioning and breaking changes?",
|
|
489
|
-
"What authentication and rate limiting model?",
|
|
490
|
-
"What error format and status code conventions?"],
|
|
491
|
-
structured_approach=["Identify resources and operations",
|
|
492
|
-
"Design URL structure and HTTP methods",
|
|
493
|
-
"Define request/response schemas",
|
|
494
|
-
"Plan versioning and error handling",
|
|
495
|
-
"Document with examples"],
|
|
496
|
-
agent_hint="plan",
|
|
497
|
-
),
|
|
498
|
-
DomainProfile(
|
|
499
|
-
id="frontend",
|
|
500
|
-
name="Frontend & UI",
|
|
501
|
-
keywords=["react", "vue", "svelte", "angular", "component", "render",
|
|
502
|
-
"state", "hook", "prop", "css", "style", "dom", "browser",
|
|
503
|
-
"responsive", "animation", "accessibility", "a11y", "ssr"],
|
|
504
|
-
phrases=["server side rendering", "client side rendering", "state management",
|
|
505
|
-
"component library", "design system", "web vitals",
|
|
506
|
-
"progressive enhancement", "single page app", "hydration",
|
|
507
|
-
"code splitting", "lazy loading"],
|
|
508
|
-
file_indicators=[".tsx", ".jsx", ".vue", ".svelte", ".css", ".scss", ".less"],
|
|
509
|
-
expert_persona=(
|
|
510
|
-
"a senior frontend architect who cares deeply about user experience, "
|
|
511
|
-
"accessibility, and performance. You've built design systems and know "
|
|
512
|
-
"that the best code is the code that makes users productive and happy."
|
|
513
|
-
),
|
|
514
|
-
thinking_frameworks=["Component composition patterns",
|
|
515
|
-
"Unidirectional data flow",
|
|
516
|
-
"Web Core Vitals (LCP, FID, CLS)",
|
|
517
|
-
"Progressive enhancement",
|
|
518
|
-
"WCAG accessibility guidelines"],
|
|
519
|
-
key_questions=["What is the target user experience?",
|
|
520
|
-
"What rendering strategy fits (SSR, CSR, ISR, SSG)?",
|
|
521
|
-
"How will we manage state (local, global, server)?",
|
|
522
|
-
"What are the accessibility requirements?",
|
|
523
|
-
"What are the performance budgets?"],
|
|
524
|
-
structured_approach=["Clarify UX requirements and constraints",
|
|
525
|
-
"Choose rendering and state management strategy",
|
|
526
|
-
"Design component hierarchy",
|
|
527
|
-
"Plan for accessibility and performance",
|
|
528
|
-
"Define testing approach (visual, interaction, a11y)"],
|
|
529
|
-
agent_hint="build",
|
|
530
|
-
),
|
|
531
|
-
DomainProfile(
|
|
532
|
-
id="algorithms",
|
|
533
|
-
name="Algorithms & Data Structures",
|
|
534
|
-
keywords=["algorithm", "complexity", "sort", "search", "graph", "tree",
|
|
535
|
-
"heap", "hash", "array", "linked list", "stack", "queue",
|
|
536
|
-
"recursive", "dynamic", "greedy", "backtrack"],
|
|
537
|
-
phrases=["time complexity", "space complexity", "dynamic programming",
|
|
538
|
-
"divide and conquer", "binary search", "breadth first",
|
|
539
|
-
"depth first", "shortest path", "minimum spanning",
|
|
540
|
-
"sliding window", "two pointer"],
|
|
541
|
-
file_indicators=[],
|
|
542
|
-
expert_persona=(
|
|
543
|
-
"a computer scientist who loves elegant solutions and rigorous analysis. "
|
|
544
|
-
"You think in terms of invariants, complexity classes, and correctness proofs. "
|
|
545
|
-
"You know that the right data structure often matters more than the algorithm."
|
|
546
|
-
),
|
|
547
|
-
thinking_frameworks=["Big-O analysis (time and space)",
|
|
548
|
-
"Problem reduction (what known problem does this map to?)",
|
|
549
|
-
"Invariant-based reasoning",
|
|
550
|
-
"Amortized analysis"],
|
|
551
|
-
key_questions=["What are the input constraints (size, range, distribution)?",
|
|
552
|
-
"What are the performance requirements?",
|
|
553
|
-
"Is there a known algorithm or pattern that applies?",
|
|
554
|
-
"Can we trade space for time (or vice versa)?",
|
|
555
|
-
"What edge cases must we handle?"],
|
|
556
|
-
structured_approach=["Understand the problem and constraints",
|
|
557
|
-
"Identify applicable patterns or known algorithms",
|
|
558
|
-
"Design solution with correctness argument",
|
|
559
|
-
"Analyze time and space complexity",
|
|
560
|
-
"Consider optimizations and edge cases"],
|
|
561
|
-
agent_hint="build",
|
|
562
|
-
),
|
|
563
|
-
DomainProfile(
|
|
564
|
-
id="code_quality",
|
|
565
|
-
name="Code Quality & Refactoring",
|
|
566
|
-
keywords=["refactor", "clean", "readab", "maintainab", "solid", "dry",
|
|
567
|
-
"smell", "debt", "pattern", "antipattern", "principle",
|
|
568
|
-
"naming", "abstraction", "duplication"],
|
|
569
|
-
phrases=["code smell", "technical debt", "design pattern", "code review",
|
|
570
|
-
"clean code", "single responsibility", "dependency injection",
|
|
571
|
-
"separation of concerns", "boy scout rule",
|
|
572
|
-
"strangler fig", "legacy code"],
|
|
573
|
-
file_indicators=[],
|
|
574
|
-
expert_persona=(
|
|
575
|
-
"a pragmatic software craftsperson who values readability over cleverness. "
|
|
576
|
-
"You refactor with purpose, not for its own sake. You know that good code "
|
|
577
|
-
"is code your teammates can understand and modify with confidence."
|
|
578
|
-
),
|
|
579
|
-
thinking_frameworks=["SOLID principles (applied pragmatically)",
|
|
580
|
-
"Refactoring patterns (Fowler)",
|
|
581
|
-
"Code smells catalog",
|
|
582
|
-
"Connascence (coupling analysis)"],
|
|
583
|
-
key_questions=["What problem is the current design causing?",
|
|
584
|
-
"Is this refactoring worth the risk and effort?",
|
|
585
|
-
"What's the minimal change that improves the situation?",
|
|
586
|
-
"How do we refactor safely (tests as safety net)?",
|
|
587
|
-
"Will this be clearer to the next person reading it?"],
|
|
588
|
-
structured_approach=["Identify the pain point or code smell",
|
|
589
|
-
"Ensure adequate test coverage before refactoring",
|
|
590
|
-
"Apply incremental, safe transformations",
|
|
591
|
-
"Verify behavior preservation after each step",
|
|
592
|
-
"Review for clarity and simplicity"],
|
|
593
|
-
agent_hint="build",
|
|
594
|
-
),
|
|
595
|
-
DomainProfile(
|
|
596
|
-
id="planning",
|
|
597
|
-
name="Project Planning & Product",
|
|
598
|
-
keywords=["plan", "roadmap", "milestone", "sprint", "epic", "story",
|
|
599
|
-
"requirement", "scope", "prioriti", "estimate", "mvp",
|
|
600
|
-
"feature", "deadline", "backlog", "stakeholder"],
|
|
601
|
-
phrases=["user story", "acceptance criteria", "definition of done",
|
|
602
|
-
"minimum viable", "project plan", "technical spec",
|
|
603
|
-
"request for comments", "design doc", "product requirement",
|
|
604
|
-
"scope creep"],
|
|
605
|
-
file_indicators=[],
|
|
606
|
-
expert_persona=(
|
|
607
|
-
"a seasoned tech lead who bridges engineering and product. You break down "
|
|
608
|
-
"ambiguous problems into concrete, shippable increments. You know that the "
|
|
609
|
-
"best plan is one the team actually follows."
|
|
610
|
-
),
|
|
611
|
-
thinking_frameworks=["User story mapping",
|
|
612
|
-
"RICE prioritization (Reach, Impact, Confidence, Effort)",
|
|
613
|
-
"MoSCoW prioritization",
|
|
614
|
-
"Incremental delivery (thin vertical slices)"],
|
|
615
|
-
key_questions=["What is the user problem we're solving?",
|
|
616
|
-
"What is the smallest thing we can ship to learn?",
|
|
617
|
-
"What are the dependencies and risks?",
|
|
618
|
-
"How will we know this succeeded?",
|
|
619
|
-
"What can we defer without losing value?"],
|
|
620
|
-
structured_approach=["Define the problem and success criteria",
|
|
621
|
-
"Break down into shippable increments",
|
|
622
|
-
"Identify dependencies, risks, and unknowns",
|
|
623
|
-
"Prioritize by value and effort",
|
|
624
|
-
"Define first concrete next steps"],
|
|
625
|
-
agent_hint="plan",
|
|
626
|
-
),
|
|
627
|
-
DomainProfile(
|
|
628
|
-
id="general",
|
|
629
|
-
name="General Discussion",
|
|
630
|
-
keywords=[],
|
|
631
|
-
phrases=[],
|
|
632
|
-
file_indicators=[],
|
|
633
|
-
expert_persona=(
|
|
634
|
-
"a knowledgeable senior engineer with broad experience across the stack. "
|
|
635
|
-
"You think clearly, communicate precisely, and always consider the broader "
|
|
636
|
-
"context before diving into details."
|
|
637
|
-
),
|
|
638
|
-
thinking_frameworks=["First principles thinking",
|
|
639
|
-
"Trade-off analysis",
|
|
640
|
-
"Systems thinking"],
|
|
641
|
-
key_questions=["What are we trying to achieve?",
|
|
642
|
-
"What are the constraints?",
|
|
643
|
-
"What are the trade-offs?"],
|
|
644
|
-
structured_approach=["Understand the question and context",
|
|
645
|
-
"Consider multiple perspectives",
|
|
646
|
-
"Analyze trade-offs",
|
|
647
|
-
"Provide a clear recommendation"],
|
|
648
|
-
agent_hint="plan",
|
|
649
|
-
),
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
@dataclass
|
|
654
|
-
class DomainDetection:
|
|
655
|
-
"""Result of domain detection."""
|
|
656
|
-
primary: DomainProfile
|
|
657
|
-
confidence: int # 0-100
|
|
658
|
-
secondary: Optional[DomainProfile] = None
|
|
659
|
-
secondary_confidence: int = 0
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def detect_domain(
|
|
663
|
-
message: str,
|
|
664
|
-
file_paths: Optional[list[str]] = None,
|
|
665
|
-
) -> DomainDetection:
|
|
666
|
-
"""Score message against all domains and return best match.
|
|
667
|
-
|
|
668
|
-
Scoring rules:
|
|
669
|
-
- keyword match: +1 per keyword found
|
|
670
|
-
- phrase match: +2 per phrase found (phrases are more specific)
|
|
671
|
-
- file indicator: +1.5 per matching file extension/pattern
|
|
672
|
-
"""
|
|
673
|
-
text = message.lower()
|
|
674
|
-
scores: dict[str, float] = {}
|
|
675
|
-
|
|
676
|
-
for domain_id, profile in DOMAIN_REGISTRY.items():
|
|
677
|
-
if domain_id == "general":
|
|
678
|
-
continue # general is the fallback
|
|
679
|
-
score = 0.0
|
|
680
|
-
|
|
681
|
-
for kw in profile.keywords:
|
|
682
|
-
if kw in text:
|
|
683
|
-
score += 1
|
|
684
|
-
|
|
685
|
-
for phrase in profile.phrases:
|
|
686
|
-
if phrase in text:
|
|
687
|
-
score += 2
|
|
688
|
-
|
|
689
|
-
if file_paths:
|
|
690
|
-
for fp in file_paths:
|
|
691
|
-
fp_lower = fp.lower()
|
|
692
|
-
name_lower = Path(fp).name.lower()
|
|
693
|
-
for indicator in profile.file_indicators:
|
|
694
|
-
ind = indicator.lower()
|
|
695
|
-
if fp_lower.endswith(ind) or ind == name_lower or ind in fp_lower:
|
|
696
|
-
score += 1.5
|
|
697
|
-
|
|
698
|
-
if score > 0:
|
|
699
|
-
scores[domain_id] = score
|
|
700
|
-
|
|
701
|
-
if not scores:
|
|
702
|
-
return DomainDetection(
|
|
703
|
-
primary=DOMAIN_REGISTRY["general"],
|
|
704
|
-
confidence=50,
|
|
705
|
-
)
|
|
706
|
-
|
|
707
|
-
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
708
|
-
best_id, best_score = ranked[0]
|
|
709
|
-
|
|
710
|
-
# Confidence: scale relative to number of matches.
|
|
711
|
-
# A score of 5+ is very confident; 1 is low.
|
|
712
|
-
confidence = min(99, int(40 + best_score * 12))
|
|
713
|
-
|
|
714
|
-
result = DomainDetection(
|
|
715
|
-
primary=DOMAIN_REGISTRY[best_id],
|
|
716
|
-
confidence=confidence,
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
# Cross-domain detection: secondary if >60% of primary
|
|
720
|
-
if len(ranked) > 1:
|
|
721
|
-
second_id, second_score = ranked[1]
|
|
722
|
-
if second_score >= best_score * 0.6:
|
|
723
|
-
result.secondary = DOMAIN_REGISTRY[second_id]
|
|
724
|
-
result.secondary_confidence = min(99, int(40 + second_score * 12))
|
|
725
|
-
|
|
726
|
-
return result
|
|
727
|
-
|
|
728
220
|
|
|
729
221
|
def build_companion_prompt(
|
|
730
222
|
message: str,
|
|
731
223
|
files: Optional[list[str]] = None,
|
|
732
224
|
domain_override: Optional[str] = None,
|
|
733
225
|
is_followup: bool = False,
|
|
734
|
-
) ->
|
|
735
|
-
"""Assemble a
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Assemble a companion prompt that auto-detects the domain.
|
|
736
228
|
|
|
737
|
-
|
|
229
|
+
The LLM identifies the domain and adopts an appropriate expert persona.
|
|
230
|
+
An optional *domain_override* hint biases the framing toward a specific field.
|
|
738
231
|
"""
|
|
739
|
-
# Detect or override domain
|
|
740
|
-
if domain_override and domain_override in DOMAIN_REGISTRY:
|
|
741
|
-
profile = DOMAIN_REGISTRY[domain_override]
|
|
742
|
-
detection = DomainDetection(primary=profile, confidence=99)
|
|
743
|
-
else:
|
|
744
|
-
detection = detect_domain(message, files)
|
|
745
|
-
profile = detection.primary
|
|
746
|
-
|
|
747
232
|
# Follow-up: lightweight prompt
|
|
748
233
|
if is_followup:
|
|
749
|
-
|
|
234
|
+
return "\n".join([
|
|
750
235
|
"## Continuing Our Discussion",
|
|
751
236
|
"",
|
|
752
237
|
message,
|
|
753
238
|
"",
|
|
754
239
|
"Remember: challenge assumptions, consider alternatives, be explicit about trade-offs.",
|
|
755
|
-
]
|
|
756
|
-
return "\n".join(parts), detection
|
|
240
|
+
])
|
|
757
241
|
|
|
758
242
|
# --- Full initial prompt ---
|
|
759
243
|
parts = []
|
|
@@ -767,59 +251,57 @@ def build_companion_prompt(
|
|
|
767
251
|
parts.append(file_context)
|
|
768
252
|
parts.append("")
|
|
769
253
|
|
|
770
|
-
#
|
|
771
|
-
|
|
772
|
-
if
|
|
773
|
-
|
|
254
|
+
# Domain hint
|
|
255
|
+
domain_hint = ""
|
|
256
|
+
if domain_override:
|
|
257
|
+
domain_hint = (
|
|
258
|
+
f"\n\nNote: the user has indicated this is about **{domain_override}** — "
|
|
259
|
+
"frame your expertise accordingly."
|
|
260
|
+
)
|
|
774
261
|
|
|
775
|
-
# Discussion setup
|
|
776
262
|
parts.append("## Discussion Setup")
|
|
777
263
|
parts.append(
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
"
|
|
264
|
+
"Determine the **specific domain of expertise** this question belongs to "
|
|
265
|
+
"(e.g., distributed systems, metagenomics, compiler design, quantitative finance, "
|
|
266
|
+
"DevOps, security, database design, or any other field).\n"
|
|
267
|
+
"\n"
|
|
268
|
+
"Then adopt the persona of a **senior practitioner with deep, hands-on "
|
|
269
|
+
"experience** in that domain. You have:\n"
|
|
270
|
+
"- Years of practical experience solving real problems in this field\n"
|
|
271
|
+
"- Deep knowledge of the key frameworks, methods, and trade-offs\n"
|
|
272
|
+
"- Strong opinions loosely held — you recommend but explain why\n"
|
|
273
|
+
"\n"
|
|
274
|
+
"Briefly state what domain you identified and what expert lens you're "
|
|
275
|
+
f"applying (one line at the top is enough).{domain_hint}"
|
|
781
276
|
)
|
|
782
277
|
parts.append("")
|
|
783
278
|
|
|
784
|
-
# Frameworks
|
|
785
|
-
parts.append(f"### Analytical Toolkit")
|
|
786
|
-
for fw in profile.thinking_frameworks:
|
|
787
|
-
parts.append(f"- {fw}")
|
|
788
|
-
parts.append("")
|
|
789
|
-
|
|
790
|
-
# Key questions
|
|
791
|
-
parts.append("### Key Questions to Consider")
|
|
792
|
-
for q in profile.key_questions:
|
|
793
|
-
parts.append(f"- {q}")
|
|
794
|
-
parts.append("")
|
|
795
|
-
|
|
796
|
-
# Collaborative ground rules
|
|
797
279
|
parts.append("## Collaborative Ground Rules")
|
|
798
|
-
parts.append("- Think out loud, share your reasoning")
|
|
280
|
+
parts.append("- Think out loud, share your reasoning step by step")
|
|
799
281
|
parts.append("- Challenge questionable assumptions — including mine")
|
|
800
282
|
parts.append("- Lay out trade-offs explicitly: what we gain, what we lose")
|
|
283
|
+
parts.append("- Name the key analytical frameworks or methods relevant to this domain")
|
|
801
284
|
parts.append("- Propose at least one alternative I haven't considered")
|
|
802
285
|
parts.append("")
|
|
803
286
|
|
|
804
|
-
|
|
805
|
-
parts.append(
|
|
806
|
-
|
|
807
|
-
|
|
287
|
+
parts.append("## Your Approach")
|
|
288
|
+
parts.append("1. Identify the domain and the core question")
|
|
289
|
+
parts.append("2. Apply domain-specific frameworks and best practices")
|
|
290
|
+
parts.append("3. Analyze trade-offs with concrete reasoning")
|
|
291
|
+
parts.append("4. Provide a clear recommendation")
|
|
808
292
|
parts.append("")
|
|
809
293
|
|
|
810
|
-
# The question
|
|
811
294
|
parts.append("## The Question")
|
|
812
295
|
parts.append(message)
|
|
813
296
|
parts.append("")
|
|
814
297
|
|
|
815
|
-
# Synthesize
|
|
816
298
|
parts.append("## Synthesize")
|
|
817
299
|
parts.append("1. Your recommendation with rationale")
|
|
818
300
|
parts.append("2. Key trade-offs")
|
|
819
301
|
parts.append("3. Risks or blind spots")
|
|
820
302
|
parts.append("4. Open questions worth exploring")
|
|
821
303
|
|
|
822
|
-
return "\n".join(parts)
|
|
304
|
+
return "\n".join(parts)
|
|
823
305
|
|
|
824
306
|
|
|
825
307
|
# Default configuration
|
|
@@ -1118,18 +600,14 @@ Set via:
|
|
|
1118
600
|
files = (files or []) + [temp_file.name]
|
|
1119
601
|
|
|
1120
602
|
# Build prompt: companion system unless _raw is set
|
|
1121
|
-
domain_info = ""
|
|
1122
603
|
if _raw:
|
|
1123
604
|
run_prompt = build_message_prompt(message, files)
|
|
1124
605
|
else:
|
|
1125
606
|
is_followup = len(session.messages) > 1
|
|
1126
|
-
run_prompt
|
|
607
|
+
run_prompt = build_companion_prompt(
|
|
1127
608
|
message, files, domain_override=domain_override,
|
|
1128
609
|
is_followup=is_followup,
|
|
1129
610
|
)
|
|
1130
|
-
domain_info = f"[Domain: {detection.primary.name}] [Confidence: {detection.confidence}%]"
|
|
1131
|
-
if detection.secondary:
|
|
1132
|
-
domain_info += f" [Also: {detection.secondary.name} ({detection.secondary_confidence}%)]"
|
|
1133
611
|
|
|
1134
612
|
args = ["run", run_prompt]
|
|
1135
613
|
|
|
@@ -1194,10 +672,7 @@ Set via:
|
|
|
1194
672
|
if reply or session.opencode_session_id:
|
|
1195
673
|
session.save(self.sessions_dir / f"{sid}.json")
|
|
1196
674
|
|
|
1197
|
-
|
|
1198
|
-
if domain_info:
|
|
1199
|
-
response = f"{domain_info}\n\n{response}"
|
|
1200
|
-
return response
|
|
675
|
+
return reply or "No response received"
|
|
1201
676
|
|
|
1202
677
|
async def plan(
|
|
1203
678
|
self,
|
|
@@ -1492,11 +967,7 @@ async def list_tools():
|
|
|
1492
967
|
},
|
|
1493
968
|
"domain": {
|
|
1494
969
|
"type": "string",
|
|
1495
|
-
"description": "
|
|
1496
|
-
"enum": ["architecture", "debugging", "performance", "security",
|
|
1497
|
-
"testing", "devops", "database", "api_design",
|
|
1498
|
-
"frontend", "algorithms", "code_quality", "planning",
|
|
1499
|
-
"general"]
|
|
970
|
+
"description": "Hint the domain of expertise (e.g., 'security', 'metagenomics', 'quantitative finance')"
|
|
1500
971
|
}
|
|
1501
972
|
},
|
|
1502
973
|
"required": ["message"]
|
|
@@ -24,29 +24,18 @@ Collaborative discussion with OpenCode models (GPT-5, Claude, Gemini). Sessions
|
|
|
24
24
|
| `/opencode set agent <name>` | Set default agent |
|
|
25
25
|
| `/opencode end` | End session |
|
|
26
26
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
When you send a message via `opencode_discuss`, the system
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| Database | "schema", "query optimization", "migration" |
|
|
40
|
-
| API Design | "REST API", "versioning", "endpoint" |
|
|
41
|
-
| Frontend | "React", "component", "SSR", "accessibility" |
|
|
42
|
-
| Algorithms | "dynamic programming", "time complexity" |
|
|
43
|
-
| Code Quality | "refactor", "SOLID", "technical debt" |
|
|
44
|
-
| Planning | "roadmap", "MVP", "user story" |
|
|
45
|
-
| General | fallback when nothing else matches |
|
|
46
|
-
|
|
47
|
-
Override detection with `domain` parameter: `opencode_discuss(message="...", domain="security")`.
|
|
48
|
-
|
|
49
|
-
The response includes the detected domain and confidence: `[Domain: Architecture] [Confidence: 92%]`.
|
|
27
|
+
## Auto-Framing Companion
|
|
28
|
+
|
|
29
|
+
When you send a message via `opencode_discuss`, the system automatically frames OpenCode as a domain expert. Rather than relying on hardcoded domain lists, the companion prompt instructs the LLM to:
|
|
30
|
+
|
|
31
|
+
1. **Self-identify the domain** from the question content
|
|
32
|
+
2. **Adopt a senior practitioner persona** with deep, hands-on experience
|
|
33
|
+
3. **Apply relevant analytical frameworks** for that domain
|
|
34
|
+
4. **Engage collaboratively** — challenging assumptions, proposing alternatives, laying out trade-offs
|
|
35
|
+
|
|
36
|
+
This works for any domain — software architecture, metagenomics, quantitative finance, linguistics, or anything else.
|
|
37
|
+
|
|
38
|
+
Optionally provide a `domain` hint to steer the framing: `opencode_discuss(message="...", domain="security")`.
|
|
50
39
|
|
|
51
40
|
Follow-up messages in an existing session get a lighter prompt that preserves the collaborative framing without repeating the full setup.
|
|
52
41
|
|
|
@@ -68,10 +57,9 @@ When user says `/opencode plan <task>`:
|
|
|
68
57
|
|
|
69
58
|
When user says `/opencode ask <question>`:
|
|
70
59
|
1. Call `opencode_discuss(message=<question>)`
|
|
71
|
-
2.
|
|
72
|
-
3. Relay the response
|
|
60
|
+
2. Relay the response
|
|
73
61
|
|
|
74
|
-
To
|
|
62
|
+
To hint a specific domain: `opencode_discuss(message=<question>, domain="security")`
|
|
75
63
|
|
|
76
64
|
### Code Review
|
|
77
65
|
|
|
@@ -119,12 +107,10 @@ Claude: Connected to OpenCode (openai/gpt-5.2-codex, plan agent). Ready.
|
|
|
119
107
|
|
|
120
108
|
User: Should we use event sourcing for our order system?
|
|
121
109
|
Claude: [calls opencode_discuss]
|
|
122
|
-
[Domain: Architecture & System Design] [Confidence: 92%]
|
|
123
110
|
[OpenCode responds as a distributed systems architect]
|
|
124
111
|
|
|
125
112
|
User: What about the security implications?
|
|
126
113
|
Claude: [calls opencode_discuss — follow-up, lighter prompt]
|
|
127
|
-
[Domain: Security & Threat Modeling] [Confidence: 76%]
|
|
128
114
|
|
|
129
115
|
User: /opencode end
|
|
130
116
|
Claude: Session ended.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Tests for the auto-framing companion prompt system."""
|
|
2
|
+
|
|
3
|
+
from opencode_bridge.server import build_companion_prompt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Initial prompt structure
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
class TestInitialPrompt:
|
|
11
|
+
def test_has_all_sections(self):
|
|
12
|
+
prompt = build_companion_prompt("How should we handle auth?")
|
|
13
|
+
assert "## Discussion Setup" in prompt
|
|
14
|
+
assert "## Collaborative Ground Rules" in prompt
|
|
15
|
+
assert "## Your Approach" in prompt
|
|
16
|
+
assert "## The Question" in prompt
|
|
17
|
+
assert "## Synthesize" in prompt
|
|
18
|
+
|
|
19
|
+
def test_contains_message(self):
|
|
20
|
+
msg = "Should we use event sourcing for orders?"
|
|
21
|
+
prompt = build_companion_prompt(msg)
|
|
22
|
+
assert msg in prompt
|
|
23
|
+
|
|
24
|
+
def test_instructs_domain_identification(self):
|
|
25
|
+
prompt = build_companion_prompt("How do we price a barrier option?")
|
|
26
|
+
assert "specific domain of expertise" in prompt
|
|
27
|
+
assert "senior practitioner" in prompt
|
|
28
|
+
|
|
29
|
+
def test_instructs_trade_off_analysis(self):
|
|
30
|
+
prompt = build_companion_prompt("Should we use Redis or Memcached?")
|
|
31
|
+
assert "trade-offs" in prompt.lower()
|
|
32
|
+
assert "challenge" in prompt.lower()
|
|
33
|
+
|
|
34
|
+
def test_works_for_software_topics(self):
|
|
35
|
+
prompt = build_companion_prompt("Should we use microservices or a monolith?")
|
|
36
|
+
assert "## The Question" in prompt
|
|
37
|
+
assert "microservices" in prompt
|
|
38
|
+
|
|
39
|
+
def test_works_for_science_topics(self):
|
|
40
|
+
prompt = build_companion_prompt(
|
|
41
|
+
"Should we use co-assembly or per-sample binning for ancient DNA metagenomes?"
|
|
42
|
+
)
|
|
43
|
+
assert "## The Question" in prompt
|
|
44
|
+
assert "co-assembly" in prompt
|
|
45
|
+
|
|
46
|
+
def test_works_for_finance_topics(self):
|
|
47
|
+
prompt = build_companion_prompt(
|
|
48
|
+
"How should we price a European barrier option with jump diffusion?"
|
|
49
|
+
)
|
|
50
|
+
assert "## The Question" in prompt
|
|
51
|
+
assert "barrier option" in prompt
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Domain override hint
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
class TestDomainOverride:
|
|
59
|
+
def test_override_included_in_prompt(self):
|
|
60
|
+
prompt = build_companion_prompt("Tell me about caching", domain_override="security")
|
|
61
|
+
assert "security" in prompt.lower()
|
|
62
|
+
|
|
63
|
+
def test_override_free_form(self):
|
|
64
|
+
prompt = build_companion_prompt(
|
|
65
|
+
"How do we handle this?", domain_override="metagenomics"
|
|
66
|
+
)
|
|
67
|
+
assert "metagenomics" in prompt
|
|
68
|
+
|
|
69
|
+
def test_no_override_no_hint(self):
|
|
70
|
+
prompt = build_companion_prompt("Tell me about caching")
|
|
71
|
+
assert "user has indicated" not in prompt
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Follow-up prompts
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class TestFollowup:
|
|
79
|
+
def test_followup_is_lightweight(self):
|
|
80
|
+
full = build_companion_prompt("How should we handle auth?")
|
|
81
|
+
followup = build_companion_prompt("What about JWT?", is_followup=True)
|
|
82
|
+
assert "Continuing Our Discussion" in followup
|
|
83
|
+
assert len(followup) < len(full)
|
|
84
|
+
|
|
85
|
+
def test_followup_does_not_have_full_sections(self):
|
|
86
|
+
followup = build_companion_prompt("What about JWT?", is_followup=True)
|
|
87
|
+
assert "## Discussion Setup" not in followup
|
|
88
|
+
assert "## Your Approach" not in followup
|
|
89
|
+
|
|
90
|
+
def test_followup_contains_message(self):
|
|
91
|
+
msg = "What about JWT vs sessions?"
|
|
92
|
+
followup = build_companion_prompt(msg, is_followup=True)
|
|
93
|
+
assert msg in followup
|
|
94
|
+
|
|
95
|
+
def test_followup_has_collaborative_reminder(self):
|
|
96
|
+
followup = build_companion_prompt("What next?", is_followup=True)
|
|
97
|
+
assert "challenge assumptions" in followup
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# File context
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
class TestFileContext:
|
|
105
|
+
def test_no_crash_with_files(self):
|
|
106
|
+
prompt = build_companion_prompt("Review this", files=["/tmp/test.py"])
|
|
107
|
+
assert "## The Question" in prompt
|
|
108
|
+
|
|
109
|
+
def test_temp_files_excluded_from_context(self):
|
|
110
|
+
prompt = build_companion_prompt(
|
|
111
|
+
"Review this", files=["/tmp/opencode_msg_abc.md"]
|
|
112
|
+
)
|
|
113
|
+
# Temp message files should not appear in file context
|
|
114
|
+
assert "opencode_msg" not in prompt.split("## The Question")[0]
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
"""Tests for the domain detection and companion prompt system."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from opencode_bridge.server import (
|
|
6
|
-
DOMAIN_REGISTRY,
|
|
7
|
-
DomainDetection,
|
|
8
|
-
DomainProfile,
|
|
9
|
-
build_companion_prompt,
|
|
10
|
-
detect_domain,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# ---------------------------------------------------------------------------
|
|
15
|
-
# Domain registry
|
|
16
|
-
# ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
class TestDomainRegistry:
|
|
19
|
-
def test_all_13_domains_registered(self):
|
|
20
|
-
assert len(DOMAIN_REGISTRY) == 13
|
|
21
|
-
|
|
22
|
-
def test_expected_domain_ids(self):
|
|
23
|
-
expected = {
|
|
24
|
-
"architecture", "debugging", "performance", "security",
|
|
25
|
-
"testing", "devops", "database", "api_design",
|
|
26
|
-
"frontend", "algorithms", "code_quality", "planning", "general",
|
|
27
|
-
}
|
|
28
|
-
assert set(DOMAIN_REGISTRY.keys()) == expected
|
|
29
|
-
|
|
30
|
-
def test_every_profile_has_required_fields(self):
|
|
31
|
-
for domain_id, profile in DOMAIN_REGISTRY.items():
|
|
32
|
-
assert profile.id == domain_id
|
|
33
|
-
assert profile.name
|
|
34
|
-
assert profile.expert_persona
|
|
35
|
-
assert len(profile.thinking_frameworks) >= 1
|
|
36
|
-
assert len(profile.key_questions) >= 1
|
|
37
|
-
assert len(profile.structured_approach) >= 1
|
|
38
|
-
assert profile.agent_hint in ("plan", "build", "explore", "general")
|
|
39
|
-
|
|
40
|
-
def test_general_has_no_keywords(self):
|
|
41
|
-
"""General is the fallback — it should never win by keyword match."""
|
|
42
|
-
g = DOMAIN_REGISTRY["general"]
|
|
43
|
-
assert g.keywords == []
|
|
44
|
-
assert g.phrases == []
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# ---------------------------------------------------------------------------
|
|
48
|
-
# Domain detection — keyword / phrase scoring
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
class TestDetectDomain:
|
|
52
|
-
def test_architecture(self):
|
|
53
|
-
d = detect_domain("Should we use event sourcing for our order system?")
|
|
54
|
-
assert d.primary.id == "architecture"
|
|
55
|
-
|
|
56
|
-
def test_debugging(self):
|
|
57
|
-
d = detect_domain("There's a race condition in the worker pool")
|
|
58
|
-
assert d.primary.id == "debugging"
|
|
59
|
-
|
|
60
|
-
def test_performance(self):
|
|
61
|
-
d = detect_domain("We need to optimize the hot path and reduce p99 latency")
|
|
62
|
-
assert d.primary.id == "performance"
|
|
63
|
-
|
|
64
|
-
def test_security(self):
|
|
65
|
-
d = detect_domain("How do we prevent SQL injection in the login form?")
|
|
66
|
-
assert d.primary.id == "security"
|
|
67
|
-
|
|
68
|
-
def test_testing(self):
|
|
69
|
-
d = detect_domain("We need better unit test coverage for the auth module")
|
|
70
|
-
assert d.primary.id == "testing"
|
|
71
|
-
|
|
72
|
-
def test_devops(self):
|
|
73
|
-
d = detect_domain("Let's set up a CI/CD pipeline with kubernetes")
|
|
74
|
-
assert d.primary.id == "devops"
|
|
75
|
-
|
|
76
|
-
def test_database(self):
|
|
77
|
-
d = detect_domain("Should we add an index on the orders table for this query?")
|
|
78
|
-
assert d.primary.id == "database"
|
|
79
|
-
|
|
80
|
-
def test_api_design(self):
|
|
81
|
-
d = detect_domain("How should we version our REST API endpoints?")
|
|
82
|
-
assert d.primary.id == "api_design"
|
|
83
|
-
|
|
84
|
-
def test_frontend(self):
|
|
85
|
-
d = detect_domain("Should this React component use SSR or client-side rendering?")
|
|
86
|
-
assert d.primary.id == "frontend"
|
|
87
|
-
|
|
88
|
-
def test_algorithms(self):
|
|
89
|
-
d = detect_domain("What's the time complexity of this dynamic programming solution?")
|
|
90
|
-
assert d.primary.id == "algorithms"
|
|
91
|
-
|
|
92
|
-
def test_code_quality(self):
|
|
93
|
-
d = detect_domain("This class violates single responsibility and has a lot of code smell")
|
|
94
|
-
assert d.primary.id == "code_quality"
|
|
95
|
-
|
|
96
|
-
def test_planning(self):
|
|
97
|
-
d = detect_domain("Let's define user stories and acceptance criteria for the MVP")
|
|
98
|
-
assert d.primary.id == "planning"
|
|
99
|
-
|
|
100
|
-
def test_general_fallback(self):
|
|
101
|
-
d = detect_domain("What is the meaning of life?")
|
|
102
|
-
assert d.primary.id == "general"
|
|
103
|
-
assert d.confidence == 50
|
|
104
|
-
|
|
105
|
-
def test_confidence_increases_with_more_matches(self):
|
|
106
|
-
few = detect_domain("Tell me about microservices")
|
|
107
|
-
many = detect_domain(
|
|
108
|
-
"Should we use microservices with event driven architecture, "
|
|
109
|
-
"a message bus, and domain driven design for our distributed system?"
|
|
110
|
-
)
|
|
111
|
-
assert many.confidence > few.confidence
|
|
112
|
-
|
|
113
|
-
def test_phrases_score_higher_than_keywords(self):
|
|
114
|
-
"""A phrase like 'event sourcing' should beat a single keyword like 'event'."""
|
|
115
|
-
kw_only = detect_domain("event")
|
|
116
|
-
phrase = detect_domain("event sourcing")
|
|
117
|
-
assert phrase.confidence >= kw_only.confidence
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# ---------------------------------------------------------------------------
|
|
121
|
-
# Cross-domain detection
|
|
122
|
-
# ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
class TestCrossDomain:
|
|
125
|
-
def test_secondary_domain_detected(self):
|
|
126
|
-
d = detect_domain("Optimize the database query performance and add an index")
|
|
127
|
-
assert d.secondary is not None
|
|
128
|
-
assert d.secondary.id != d.primary.id
|
|
129
|
-
assert d.secondary_confidence > 0
|
|
130
|
-
|
|
131
|
-
def test_no_secondary_when_dominant(self):
|
|
132
|
-
d = detect_domain("What is the meaning of life?")
|
|
133
|
-
assert d.secondary is None
|
|
134
|
-
|
|
135
|
-
def test_secondary_ids_are_valid(self):
|
|
136
|
-
d = detect_domain("Deploy a kubernetes cluster with monitoring and CI/CD pipeline alerts")
|
|
137
|
-
if d.secondary:
|
|
138
|
-
assert d.secondary.id in DOMAIN_REGISTRY
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# ---------------------------------------------------------------------------
|
|
142
|
-
# File indicator scoring
|
|
143
|
-
# ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
class TestFileIndicators:
|
|
146
|
-
def test_dockerfile_triggers_devops(self):
|
|
147
|
-
d = detect_domain("How should we configure this?", file_paths=["/app/Dockerfile"])
|
|
148
|
-
assert d.primary.id == "devops"
|
|
149
|
-
|
|
150
|
-
def test_yaml_triggers_devops(self):
|
|
151
|
-
d = detect_domain("How should we set this up?", file_paths=["/app/deploy.yaml"])
|
|
152
|
-
# yaml can match devops or architecture; both have .yaml
|
|
153
|
-
assert d.primary.id in ("devops", "architecture")
|
|
154
|
-
|
|
155
|
-
def test_sql_triggers_database(self):
|
|
156
|
-
d = detect_domain("Review this file", file_paths=["/db/migration.sql"])
|
|
157
|
-
assert d.primary.id == "database"
|
|
158
|
-
|
|
159
|
-
def test_proto_triggers_architecture(self):
|
|
160
|
-
d = detect_domain("Review this", file_paths=["/api/service.proto"])
|
|
161
|
-
assert d.primary.id in ("architecture", "api_design")
|
|
162
|
-
|
|
163
|
-
def test_tsx_triggers_frontend(self):
|
|
164
|
-
d = detect_domain("Review this component", file_paths=["/src/Button.tsx"])
|
|
165
|
-
assert d.primary.id == "frontend"
|
|
166
|
-
|
|
167
|
-
def test_test_file_triggers_testing(self):
|
|
168
|
-
d = detect_domain("Check this", file_paths=["/src/auth_test.py"])
|
|
169
|
-
assert d.primary.id == "testing"
|
|
170
|
-
|
|
171
|
-
def test_multiple_files_accumulate(self):
|
|
172
|
-
d = detect_domain(
|
|
173
|
-
"Review these",
|
|
174
|
-
file_paths=["/app/Dockerfile", "/app/deploy.yaml", "/app/k8s.yml"],
|
|
175
|
-
)
|
|
176
|
-
assert d.primary.id == "devops"
|
|
177
|
-
assert d.confidence > detect_domain(
|
|
178
|
-
"Review these", file_paths=["/app/Dockerfile"]
|
|
179
|
-
).confidence
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# ---------------------------------------------------------------------------
|
|
183
|
-
# Companion prompt generation
|
|
184
|
-
# ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
class TestBuildCompanionPrompt:
|
|
187
|
-
def test_initial_prompt_has_all_sections(self):
|
|
188
|
-
prompt, det = build_companion_prompt("How should we handle auth?")
|
|
189
|
-
assert "## Discussion Setup" in prompt
|
|
190
|
-
assert "### Analytical Toolkit" in prompt
|
|
191
|
-
assert "### Key Questions to Consider" in prompt
|
|
192
|
-
assert "## Collaborative Ground Rules" in prompt
|
|
193
|
-
assert "## Approach" in prompt
|
|
194
|
-
assert "## The Question" in prompt
|
|
195
|
-
assert "## Synthesize" in prompt
|
|
196
|
-
|
|
197
|
-
def test_initial_prompt_contains_message(self):
|
|
198
|
-
msg = "Should we use event sourcing for orders?"
|
|
199
|
-
prompt, _ = build_companion_prompt(msg)
|
|
200
|
-
assert msg in prompt
|
|
201
|
-
|
|
202
|
-
def test_initial_prompt_contains_persona(self):
|
|
203
|
-
prompt, det = build_companion_prompt("How to prevent SQL injection?")
|
|
204
|
-
assert det.primary.expert_persona in prompt
|
|
205
|
-
|
|
206
|
-
def test_followup_is_lightweight(self):
|
|
207
|
-
full, _ = build_companion_prompt("How should we handle auth?")
|
|
208
|
-
followup, _ = build_companion_prompt("What about JWT?", is_followup=True)
|
|
209
|
-
assert "Continuing Our Discussion" in followup
|
|
210
|
-
assert len(followup) < len(full)
|
|
211
|
-
|
|
212
|
-
def test_followup_does_not_have_full_sections(self):
|
|
213
|
-
followup, _ = build_companion_prompt("What about JWT?", is_followup=True)
|
|
214
|
-
assert "## Discussion Setup" not in followup
|
|
215
|
-
assert "### Analytical Toolkit" not in followup
|
|
216
|
-
|
|
217
|
-
def test_followup_contains_message(self):
|
|
218
|
-
msg = "What about JWT vs sessions?"
|
|
219
|
-
followup, _ = build_companion_prompt(msg, is_followup=True)
|
|
220
|
-
assert msg in followup
|
|
221
|
-
|
|
222
|
-
def test_domain_override(self):
|
|
223
|
-
prompt, det = build_companion_prompt("Tell me about caching", domain_override="security")
|
|
224
|
-
assert det.primary.id == "security"
|
|
225
|
-
assert det.confidence == 99
|
|
226
|
-
|
|
227
|
-
def test_invalid_domain_override_falls_back_to_detection(self):
|
|
228
|
-
prompt, det = build_companion_prompt(
|
|
229
|
-
"Optimize the query", domain_override="nonexistent_domain"
|
|
230
|
-
)
|
|
231
|
-
# Should fall back to auto-detection, not crash
|
|
232
|
-
assert det.primary.id != "nonexistent_domain"
|
|
233
|
-
assert det.primary.id in DOMAIN_REGISTRY
|
|
234
|
-
|
|
235
|
-
def test_file_context_included(self):
|
|
236
|
-
prompt, _ = build_companion_prompt(
|
|
237
|
-
"Review this", files=["/tmp/test.py"]
|
|
238
|
-
)
|
|
239
|
-
# File context section is only added if file exists; just ensure no crash
|
|
240
|
-
assert "## The Question" in prompt
|
|
241
|
-
|
|
242
|
-
def test_cross_domain_note_in_prompt(self):
|
|
243
|
-
prompt, det = build_companion_prompt(
|
|
244
|
-
"Optimize the database query performance and add an index"
|
|
245
|
-
)
|
|
246
|
-
if det.secondary:
|
|
247
|
-
assert "also touches on" in prompt.lower()
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
# ---------------------------------------------------------------------------
|
|
251
|
-
# DomainDetection dataclass
|
|
252
|
-
# ---------------------------------------------------------------------------
|
|
253
|
-
|
|
254
|
-
class TestDomainDetection:
|
|
255
|
-
def test_confidence_range(self):
|
|
256
|
-
for msg in [
|
|
257
|
-
"event sourcing microservices",
|
|
258
|
-
"debug this bug",
|
|
259
|
-
"What is life?",
|
|
260
|
-
]:
|
|
261
|
-
d = detect_domain(msg)
|
|
262
|
-
assert 0 <= d.confidence <= 100
|
|
263
|
-
|
|
264
|
-
def test_secondary_confidence_less_than_or_equal_primary(self):
|
|
265
|
-
d = detect_domain("Optimize the database query and add an index")
|
|
266
|
-
if d.secondary:
|
|
267
|
-
assert d.secondary_confidence <= d.confidence
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|