## 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>
|
||
|---|---|---|
| .. | ||
| README.md | ||
| e2e-ci-analyst.md | ||
| e2e-ci-architect.md | ||
| e2e-ci-engineer.md | ||
| e2e-ci-healer.md | ||
| e2e-ci-refiner.md | ||
| e2e-ci-responder.md | ||
| e2e-ci-sentinel.md | ||
| e2e-ci-triage.md | ||
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 emitsplaywright-registration.jsonand a deterministic step applies a singlerun_filesappend. - Path/identifier validation before shelling out.
spec_filenamemust match^[A-Za-z0-9._-]+\.spec\.js$;spec_pathis 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 requestscontents: 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.log→testLogger, 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.