openobserve/.opencode/agents/e2e_ci_council_of_agents
Shrinath Rao 0ee8a667c2
test: E2E Council Refiner — remediate Sentinel findings instead of dead-ending (#12876)
## Why

The E2E Council blocked **#12857** (no test PR opened) because the
post-heal **Sentinel re-audit found 2 critical quality issues** — even
though **9/10 tests passed**. The tests were **not worthless**; 2 just
*under-claimed* (the body asserted something weaker than the test name
promised):

1. `"should not persist FTS default across page reload"` — re-searches
after reload and asserts the column visible, which passes whether it
*persisted* or *re-resolved*. Never verifies non-persistence.
2. `"should clear FTS marker and re-resolve for new stream on stream
change"` — only checks "table rendered", never that FTS actually
re-resolved.

Blocking was correct (green ≠ meaningful), but a **dead-end block throws
away the other 90%**. The pipeline should **fix the problem and make it
right**, not give up. A no-PR outcome should be a *true last resort*.

## What — The Refiner (Phase 5.5)

A remediation loop **inside `verify_heal`**, after the Sentinel re-audit
and before the quality gate (**reuses the already-booted binary** — no
rebuild):

- **Only fires when needed.** If the re-audit is clean (PASS / 0
critical), the step reads the verdict, sets `ran=false`, and exits —
**no DeepSeek call, no test re-run**. Cheap on the happy path.
- **Strengthen-first.** Feeds the exact Sentinel findings to the
Refiner, which **adds/tightens assertions** so each test verifies what
its name promises → re-runs on the live binary (**green must hold**) →
re-checks assertion-integrity → re-audits. Capped at **2 rounds**.
- **Anti-laundering (the whole game).** Assertions may only be
**ADDED/strengthened — never dropped, inverted in place, or
renamed-to-dodge** a missing assertion. If strengthening goes red
because the **feature isn't wired**, it takes the honest `test.fixme` +
evidence exit (no green-wash).
- **`assert_integrity.py --strengthen`**: permits *added* negative
matchers (assert-absence is legit strengthening) while **still blocking
any dropped assertion** (removed/skipped guards + all static checks
stay). Unit-proven in the commit.
- **Quality gate** now judges the **post-refine** state; blocks only if
the Refiner couldn't keep the spec green honestly.
- **No-PR report** now phrases a block as a true last resort ("the
Refiner attempted to strengthen … but could not resolve every finding
honestly").

### Reuse path (clobber off)
`verify_heal` now also runs on **reuse runs**, and the "already covered"
short-circuit is suppressed on reuse — so **re-running `/e2e` on a
previously-BLOCKED spec re-validates + refines the existing tests into a
PR** instead of stopping at "already covered". No new tests are added on
reuse; the Refiner only strengthens what's there. (Cost: a reuse run
rebuilds even when tests were already perfect — acceptable since re-runs
are explicit; can add a "skip if a green test PR already exists" guard
later.)

## Validation
- `assert_integrity.py` unit-checked: added negative → FAIL without
`--strengthen`, PASS with it; dropped assertion → still FAIL with
`--strengthen`.
- Workflow YAML validated; step order confirmed **Sentinel re-audit →
Refiner → Quality gate**.
- Live end-to-end best validated by re-running `/e2e` on **#12857 with
clobber off** — the Refiner should strengthen the 2 flagged tests and
open the PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:20:31 +00:00
..
README.md ci: add e2e_ci_council_of_agents triage pipeline (dry-run, manual-first) (#12652) 2026-06-15 06:10:42 +00:00
e2e-ci-analyst.md ci(e2e-council): test-quality gate — stop certifying green-but-worthless specs (#12722) 2026-06-21 09:30:03 +00:00
e2e-ci-architect.md ci(e2e-council): test-quality gate — stop certifying green-but-worthless specs (#12722) 2026-06-21 09:30:03 +00:00
e2e-ci-engineer.md ci(e2e-council): test-quality gate — stop certifying green-but-worthless specs (#12722) 2026-06-21 09:30:03 +00:00
e2e-ci-healer.md ci(e2e-council): test-quality gate — stop certifying green-but-worthless specs (#12722) 2026-06-21 09:30:03 +00:00
e2e-ci-refiner.md test: E2E Council Refiner — remediate Sentinel findings instead of dead-ending (#12876) 2026-06-25 09:20:31 +00:00
e2e-ci-responder.md ci(test): e2e-council enrichment — summary table, auto-review, + CLA bot-identity fix (#12684) 2026-06-16 05:46:08 +00:00
e2e-ci-sentinel.md ci(e2e-council): test-quality gate — stop certifying green-but-worthless specs (#12722) 2026-06-21 09:30:03 +00:00
e2e-ci-triage.md test(e2e-council): scope existing_tests_in_diff to E2E specs only (#12826) 2026-06-23 08:36:19 +00:00

README.md

e2e_ci_council_of_agents

CI-adjusted version of the Council of Agents E2E test-generation pipeline, built to run non-interactively in GitHub Actions via OpenCode on DeepSeek-V4-Pro.

This is the CI copy. The interactive local copy lives in .claude/commands/council_of_agents/ (Claude Code). The two are intentionally separate (see the handoff doc §9.3) — keep the prompt bodies aligned, but these files carry CI-specific wiring: standalone invocation, file-based handoff, no interactive prompts.

Spec / source of truth: tests/ui-testing/MD_Files/council-of-agents-export/CICD integration/council-ci-automation-handoff.md


What's different from the local Council agents

Local (Claude Code) CI (this folder)
mode: subagent, spawned by the Orchestrator in one session mode: primary, each run as its own opencode run --agent ... invocation
Conversation-based handoff File-based handoff via docs/test_generator/ artifacts
Interactive checkpoints ("DO NOT proceed until user approves") No interactive prompts — agents decide and write artifacts
Engineer runs + heals tests inline (--headed) Engineer generates only; the Healer runs them headless in a later job
Enterprise detection inside each agent Triage gates ENT/OSS once up front; ENT is skipped in v1
Hardcoded local absolute paths Repo-relative paths only
Scribe (Phase 6) part of pipeline Scribe excluded from CI

Agents (invocation order)

Order Agent file OpenCode agent Job Role
0 e2e-ci-triage.md e2e-ci-triage 1 Classify ENT/OSS, needs-e2e, skip gate. Writes run-context.json.
1 e2e-ci-analyst.md e2e-ci-analyst 2 Analyze source → Feature Design Document.
2 e2e-ci-architect.md e2e-ci-architect 2 Feature doc → prioritized Test Plan.
3 e2e-ci-engineer.md e2e-ci-engineer 2 Test Plan → spec file(s) + page objects + register in playwright.yml. No execution.
4 e2e-ci-sentinel.md e2e-ci-sentinel 2 Audit generated code. Blocks on critical issues.
5 e2e-ci-healer.md e2e-ci-healer 3 Run tests on local OSS binary, fix until passing (max 3 iterations, <6 min/test), Sentinel re-audit.

Sequencing lives in the workflow YAML (it re-implements the Orchestrator). There is no Orchestrator agent in CI — the YAML calls each opencode run in order.


File-based handoff contract

All artifacts live under docs/test_generator/. Each agent reads its inputs and writes its outputs as files; nothing is passed via conversation. Between GitHub jobs, the whole docs/test_generator/ tree (plus any edited test/page-object files) is shuttled with upload-artifact / download-artifact.

docs/test_generator/
  ci/
    diff.patch              # Workflow writes the change diff (untrusted input).
    feature_hint.txt        # Workflow writes the optional hint (untrusted input).
    trigger_comment.txt     # Workflow writes the triggering comment (untrusted input).
    run-context.json        # Triage writes; ALL downstream agents read. The single source of run state.
    triage.json             # Full triage output (for the PR comment in dry-run).
    playwright-registration.json  # Engineer writes: { group, spec_filename, create_group }. A
                            # DETERMINISTIC (non-LLM) step applies the run_files edit — agents
                            # never edit .github/workflows/ themselves.
    sentinel-verdict.json   # Sentinel writes: { verdict: PASS|FAIL, critical_count }.
    heal-result.json        # Healer writes: { status: passing|failing, iterations }.
  features/<slug>-feature.md            # Analyst
  test-plans/<slug>-test-plan.md        # Architect
  generation-reports/<slug>-generation.md  # Engineer (what it created + the playwright.yml edit)
  audit-reports/<slug>-audit.md         # Sentinel
  execution-reports/<slug>-execution.md # Healer

The generated test itself is written to the repo, not under docs/: tests/ui-testing/playwright-tests/<Area>/<spec_filename> (+ any page-object edits under tests/ui-testing/pages/).

run-context.json schema

{
  "feature_slug": "share-link",
  "feature_title": "Logs Share Link",
  "area": "Logs",
  "edition": "oss",
  "needs_e2e": true,
  "needs_api": false,
  "spec_filename": "shareLink.spec.js",
  "spec_path": "tests/ui-testing/playwright-tests/Logs/shareLink.spec.js",
  "playwright_group": "Logs-Core",
  "source_files": ["web/src/plugins/logs/..."],
  "skip": false,
  "skip_reason": ""
}

playwright_group = the testfolder matrix group in .github/workflows/playwright.yml whose run_files array the Engineer appends the spec filename to.


Security model

These agents process attacker-controllable input (PR diffs, comments, hints — anyone can open a PR). Hardening built into the design:

  • Untrusted input is data, not instructions. The diff/comment/hint reach agents as files, and every agent that reads them is told to treat their content as inert data and ignore any embedded instructions ("prompt injection").
  • No LLM edits to CI workflow files. The Engineer never edits .github/workflows/**; it emits playwright-registration.json and a deterministic step applies a single run_files append.
  • Path/identifier validation before shelling out. spec_filename must match ^[A-Za-z0-9._-]+\.spec\.js$; spec_path is regex-validated before the Healer runs it (and again in the workflow).
  • Least-privilege CI. The live triage job runs with contents: read; only the PR-back job requests contents: write.
  • Human review remains the backstop. All generated code lands in a PR a human reviews before merge; Sentinel auto-fixes are a tiny whitelist (add import, console.logtestLogger, tag @ prefix).

Scope (v1)

  • OSS only. ENT features are skipped at triage (no generation, no PR).
  • Scribe excluded. TestDino upload rides the generated PR's own existing CI.
  • Local OSS binary is the Healer's only run target.
  • On-demand agents (Inspector / Gatekeeper / Guardian / API-Smith) are out of scope.

Invocation (CI)

Each agent runs standalone, e.g.:

opencode run --agent e2e-ci-analyst --model deepseek/deepseek-v4-pro \
  "Generate the Feature Design Document for the feature described in docs/test_generator/ci/run-context.json"

The model is passed at invocation (not pinned in frontmatter) so the same files can run on a different provider if needed. The DeepSeek provider is configured in opencode.jsonc.

Discovery caveat: verify against current OpenCode docs whether agents in a nested subfolder of .opencode/agents/ are auto-discovered. If not, either flatten the files or register them explicitly / invoke by prompt-file path in the workflow.